aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server
diff options
context:
space:
mode:
authorNassim Jahnke <[email protected]>2024-12-03 18:27:11 +0100
committerNassim Jahnke <[email protected]>2024-12-03 18:32:16 +0100
commit172c7dc7e7ee96fceaa2586d9cd9d892184f9a39 (patch)
tree9c50b19e41f4f910533c7470a22b04072e2f5365 /patches/server
parentc0a3d51ab35930e410fcd9752ceaff6c3f581c24 (diff)
downloadPaper-172c7dc7e7ee96fceaa2586d9cd9d892184f9a39.tar.gz
Paper-172c7dc7e7ee96fceaa2586d9cd9d892184f9a39.zip
Work
Diffstat (limited to 'patches/server')
-rw-r--r--patches/server/0001-Setup-Gradle-project.patch72
-rw-r--r--patches/server/0002-Remap-fixes.patch224
-rw-r--r--patches/server/0003-Build-system-changes.patch185
-rw-r--r--patches/server/0004-Test-changes.patch567
-rw-r--r--patches/server/0005-Paper-config-files.patch5525
-rw-r--r--patches/server/0006-MC-Dev-fixes.patch150
-rw-r--r--patches/server/0007-ConcurrentUtil.patch10547
-rw-r--r--patches/server/0008-CB-fixes.patch157
-rw-r--r--patches/server/0009-MC-Utils.patch6589
-rw-r--r--patches/server/0010-Adventure.patch6280
-rw-r--r--patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch707
-rw-r--r--patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch71
-rw-r--r--patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch47
-rw-r--r--patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch44
-rw-r--r--patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch581
-rw-r--r--patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch246
-rw-r--r--patches/server/0017-Paper-command.patch665
-rw-r--r--patches/server/0018-Paper-Metrics.patch731
-rw-r--r--patches/server/0019-Paper-Plugins.patch8168
-rw-r--r--patches/server/0020-Plugin-remapping.patch1917
-rw-r--r--patches/server/0021-Hook-into-CB-plugin-rewrites.patch185
-rw-r--r--patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch740
-rw-r--r--patches/server/0023-Further-improve-server-tick-loop.patch233
-rw-r--r--patches/server/0024-Remove-Spigot-timings.patch967
-rw-r--r--patches/server/0025-Add-command-line-option-to-load-extra-plugin-jars-no.patch65
-rw-r--r--patches/server/0026-Support-components-in-ItemMeta.patch83
-rw-r--r--patches/server/0027-Configurable-cactus-bamboo-and-reed-growth-height.patch92
-rw-r--r--patches/server/0028-Configurable-baby-zombie-movement-speed.patch31
-rw-r--r--patches/server/0029-Configurable-fishing-time-ranges.patch30
-rw-r--r--patches/server/0030-Allow-nerfed-mobs-to-jump.patch47
-rw-r--r--patches/server/0031-Add-configurable-entity-despawn-distances.patch47
-rw-r--r--patches/server/0032-Drop-falling-block-and-tnt-entities-at-the-specified.patch62
-rw-r--r--patches/server/0033-Expose-server-build-information.patch758
-rw-r--r--patches/server/0034-Player-affects-spawning-API.patch158
-rw-r--r--patches/server/0035-Only-refresh-abilities-if-needed.patch25
-rw-r--r--patches/server/0036-Entity-Origin-API.patch121
-rw-r--r--patches/server/0037-Prevent-block-entity-and-entity-crashes.patch66
-rw-r--r--patches/server/0038-Configurable-top-of-nether-void-damage.patch49
-rw-r--r--patches/server/0039-Check-online-mode-before-converting-and-renaming-pla.patch19
-rw-r--r--patches/server/0040-Add-more-entities-to-activation-range-ignore-list.patch20
-rw-r--r--patches/server/0041-Configurable-end-credits.patch18
-rw-r--r--patches/server/0042-Fix-lag-from-explosions-processing-dead-entities.patch19
-rw-r--r--patches/server/0043-Optimize-explosions.patch134
-rw-r--r--patches/server/0044-Disable-explosion-knockback.patch28
-rw-r--r--patches/server/0045-Disable-thunder.patch19
-rw-r--r--patches/server/0046-Disable-ice-and-snow.patch24
-rw-r--r--patches/server/0047-Configurable-mob-spawner-tick-rate.patch39
-rw-r--r--patches/server/0048-Use-null-Locale-by-default.patch53
-rw-r--r--patches/server/0049-Add-BeaconEffectEvent.patch66
-rw-r--r--patches/server/0050-Configurable-container-update-tick-rate.patch31
-rw-r--r--patches/server/0051-Use-UserCache-for-player-heads.patch25
-rw-r--r--patches/server/0052-Disable-spigot-tick-limiters.patch21
-rw-r--r--patches/server/0053-Fix-spawn-location-event-changing-location.patch24
-rw-r--r--patches/server/0054-Configurable-Disabling-Cat-Chest-Detection.patch23
-rw-r--r--patches/server/0055-Improve-Player-chat-API-handling.patch78
-rw-r--r--patches/server/0056-All-chunks-are-slime-spawn-chunks-toggle.patch32
-rw-r--r--patches/server/0057-Expose-server-CommandMap.patch18
-rw-r--r--patches/server/0058-Be-a-bit-more-informative-in-maxHealth-exception.patch24
-rw-r--r--patches/server/0059-Player-Tab-List-and-Title-APIs.patch109
-rw-r--r--patches/server/0060-Add-configurable-portal-search-radius.patch38
-rw-r--r--patches/server/0061-Add-velocity-warnings.patch85
-rw-r--r--patches/server/0062-Add-exception-reporting-event.patch207
-rw-r--r--patches/server/0063-Disable-Scoreboards-for-non-players-by-default.patch36
-rw-r--r--patches/server/0064-Add-methods-for-working-with-arrows-stuck-in-living-.patch60
-rw-r--r--patches/server/0065-Complete-resource-pack-API.patch49
-rw-r--r--patches/server/0066-Default-loading-permissions.yml-before-plugins.patch38
-rw-r--r--patches/server/0067-Allow-Reloading-of-Custom-Permissions.patch35
-rw-r--r--patches/server/0068-Remove-Metadata-on-reload.patch29
-rw-r--r--patches/server/0069-Handle-Item-Meta-Inconsistencies.patch303
-rw-r--r--patches/server/0070-Configurable-Non-Player-Arrow-Despawn-Rate.patch20
-rw-r--r--patches/server/0071-Add-World-Util-Methods.patch34
-rw-r--r--patches/server/0072-Custom-replacement-for-eaten-items.patch48
-rw-r--r--patches/server/0073-handle-NaN-health-absorb-values-and-repair-bad-data.patch57
-rw-r--r--patches/server/0074-Use-a-Shared-Random-for-Entities.patch113
-rw-r--r--patches/server/0075-Configurable-spawn-chances-for-skeleton-horses.patch19
-rw-r--r--patches/server/0076-Only-process-BlockPhysicsEvent-if-a-plugin-has-a-lis.patch70
-rw-r--r--patches/server/0077-Entity-AddTo-RemoveFrom-World-Events.patch26
-rw-r--r--patches/server/0078-Configurable-Chunk-Inhabited-Time.patch30
-rw-r--r--patches/server/0079-EntityPathfindEvent.patch154
-rw-r--r--patches/server/0080-Sanitise-RegionFileCache-and-make-configurable.patch25
-rw-r--r--patches/server/0081-Do-not-load-chunks-for-Pathfinding.patch24
-rw-r--r--patches/server/0082-Add-PlayerUseUnknownEntityEvent.patch78
-rw-r--r--patches/server/0083-Configurable-random-tick-rates-for-blocks.patch35
-rw-r--r--patches/server/0084-Fix-Cancelling-BlockPlaceEvent-triggering-physics.patch24
-rw-r--r--patches/server/0085-Optimize-DataBits.patch118
-rw-r--r--patches/server/0086-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch50
-rw-r--r--patches/server/0087-Configurable-Player-Collision.patch114
-rw-r--r--patches/server/0088-Add-handshake-event-to-allow-plugins-to-handle-clien.patch56
-rw-r--r--patches/server/0089-Configurable-RCON-IP-address.patch47
-rw-r--r--patches/server/0090-EntityRegainHealthEvent-isFastRegen-API.patch42
-rw-r--r--patches/server/0091-Add-ability-to-configure-frosted_ice-properties.patch32
-rw-r--r--patches/server/0092-remove-null-possibility-for-getServer-singleton.patch38
-rw-r--r--patches/server/0093-Don-t-save-empty-scoreboard-teams-to-scoreboard.dat.patch18
-rw-r--r--patches/server/0094-LootTable-API-and-replenishable-lootables.patch964
-rw-r--r--patches/server/0095-System-property-for-disabling-watchdoge.patch19
-rw-r--r--patches/server/0096-Async-GameProfileCache-saving.patch86
-rw-r--r--patches/server/0097-Optional-TNT-doesn-t-move-in-water.patch50
-rw-r--r--patches/server/0098-Faster-redstone-torch-rapid-clock-removal.patch68
-rw-r--r--patches/server/0099-Add-server-name-parameter.patch25
-rw-r--r--patches/server/0100-Fix-global-sound-handling.patch101
100 files changed, 51485 insertions, 36 deletions
diff --git a/patches/server/0001-Setup-Gradle-project.patch b/patches/server/0001-Setup-Gradle-project.patch
index 4319f4cd05..0a6c8eafdb 100644
--- a/patches/server/0001-Setup-Gradle-project.patch
+++ b/patches/server/0001-Setup-Gradle-project.patch
@@ -28,7 +28,7 @@ index 37dab9e868dbfb019c271a547d975a48ad1cb571..3811c0d849a3eb028ed1a6b7a2d4747f
+/.factorypath
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
-index 0000000000000000000000000000000000000000..6ef457b8ea6ff9b89cb74ecbdca20731d9f94e97
+index 0000000000000000000000000000000000000000..36e6426438865213d2e5e11ee3b1d6e538d475f7
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,131 @@
@@ -42,12 +42,12 @@ index 0000000000000000000000000000000000000000..6ef457b8ea6ff9b89cb74ecbdca20731
+dependencies {
+ implementation(project(":paper-api"))
+ implementation("jline:jline:2.12.1")
-+ implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") {
++ implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") {
+ exclude(group = "org.apache.logging.log4j", module = "log4j-api")
+ }
+ implementation("org.ow2.asm:asm-commons:9.7.1")
+ implementation("commons-lang:commons-lang:2.6")
-+ runtimeOnly("org.xerial:sqlite-jdbc:3.46.1.3")
++ runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
+ runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
+
+ runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6")
@@ -62,7 +62,7 @@ index 0000000000000000000000000000000000000000..6ef457b8ea6ff9b89cb74ecbdca20731
+}
+
+paperweight {
-+ craftBukkitPackageVersion.set("v1_21_R2") // also needs to be updated in MappingEnvironment
++ craftBukkitPackageVersion.set("v1_21_R3") // also needs to be updated in MappingEnvironment
+}
+
+tasks.jar {
@@ -165,7 +165,7 @@ index 0000000000000000000000000000000000000000..6ef457b8ea6ff9b89cb74ecbdca20731
+}
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
-index c2973479d13a5d2898523cf5f246db39c2ea48e6..0000000000000000000000000000000000000000
+index 8fdbc360f03b7ad561be4497f0793e81aa2e170b..0000000000000000000000000000000000000000
--- a/pom.xml
+++ /dev/null
@@ -1,691 +0,0 @@
@@ -175,7 +175,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <groupId>org.spigotmc</groupId>
- <artifactId>spigot</artifactId>
- <packaging>jar</packaging>
-- <version>1.21.3-R0.1-SNAPSHOT</version>
+- <version>1.21.4-R0.1-SNAPSHOT</version>
- <name>Spigot</name>
- <url>https://www.spigotmc.org/</url>
-
@@ -192,7 +192,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <api.version>unknown</api.version>
- <bt.name>git</bt.name>
-- <minecraft_version>1_21_R2</minecraft_version>
+- <minecraft_version>1_21_R3</minecraft_version>
- <maven.compiler.release>21</maven.compiler.release>
- </properties>
-
@@ -226,7 +226,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-iostreams</artifactId>
-- <version>2.22.1</version>
+- <version>2.24.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -257,7 +257,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>com.github.oshi</groupId>
- <artifactId>oshi-core</artifactId>
-- <version>6.4.10</version>
+- <version>6.6.5</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -269,13 +269,13 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>com.microsoft.azure</groupId>
- <artifactId>msal4j</artifactId>
-- <version>1.15.0</version>
+- <version>1.17.2</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>com.mojang</groupId>
- <artifactId>authlib</artifactId>
-- <version>6.0.55</version>
+- <version>6.0.57</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -299,7 +299,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>com.mojang</groupId>
- <artifactId>logging</artifactId>
-- <version>1.4.9</version>
+- <version>1.5.10</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -317,111 +317,111 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>com.nimbusds</groupId>
- <artifactId>nimbus-jose-jwt</artifactId>
-- <version>9.37.3</version>
+- <version>9.40</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>com.nimbusds</groupId>
- <artifactId>oauth2-oidc-sdk</artifactId>
-- <version>11.9.1</version>
+- <version>11.18</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
-- <version>2.15.1</version>
+- <version>2.17.0</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-buffer</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-codec</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-common</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-handler</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-resolver</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport-classes-epoll</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport-native-epoll</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <classifier>linux-x86_64</classifier>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport-native-epoll</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <classifier>linux-aarch_64</classifier>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport-native-unix-common</artifactId>
-- <version>4.1.97.Final</version>
+- <version>4.1.115.Final</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>it.unimi.dsi</groupId>
- <artifactId>fastutil</artifactId>
-- <version>8.5.12</version>
+- <version>8.5.15</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.java.dev.jna</groupId>
- <artifactId>jna</artifactId>
-- <version>5.14.0</version>
+- <version>5.15.0</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.java.dev.jna</groupId>
- <artifactId>jna-platform</artifactId>
-- <version>5.14.0</version>
+- <version>5.15.0</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.minidev</groupId>
- <artifactId>accessors-smart</artifactId>
-- <version>2.5.0</version>
+- <version>2.5.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.minidev</groupId>
- <artifactId>json-smart</artifactId>
-- <version>2.5.0</version>
+- <version>2.5.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -433,25 +433,25 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
-- <version>3.14.0</version>
+- <version>3.17.0</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-api</artifactId>
-- <version>2.22.1</version>
+- <version>2.24.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-core</artifactId>
-- <version>2.22.1</version>
+- <version>2.24.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-slf4j2-impl</artifactId>
-- <version>2.22.1</version>
+- <version>2.24.1</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
@@ -463,7 +463,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
-- <version>2.0.9</version>
+- <version>2.0.16</version>
- <scope>compile</scope>
- </dependency>
- <!-- End Mojang depends -->
@@ -490,7 +490,7 @@ index c2973479d13a5d2898523cf5f246db39c2ea48e6..00000000000000000000000000000000
- <dependency>
- <groupId>org.xerial</groupId>
- <artifactId>sqlite-jdbc</artifactId>
-- <version>3.46.1.3</version>
+- <version>3.47.0.0</version>
- <scope>runtime</scope>
- </dependency>
- <dependency>
diff --git a/patches/server/0002-Remap-fixes.patch b/patches/server/0002-Remap-fixes.patch
new file mode 100644
index 0000000000..4ccde45041
--- /dev/null
+++ b/patches/server/0002-Remap-fixes.patch
@@ -0,0 +1,224 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Kyle Wood <[email protected]>
+Date: Fri, 11 Jun 2021 05:25:03 -0500
+Subject: [PATCH] Remap fixes
+
+
+diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
+index c05eadf5f2ea2b9a66b4c63ca78db9a11bc585f3..d4bff51e5fe0c76d6e3832f48067b6051b217ab7 100644
+--- a/src/main/java/net/minecraft/core/BlockPos.java
++++ b/src/main/java/net/minecraft/core/BlockPos.java
+@@ -324,9 +324,11 @@ public class BlockPos extends Vec3i {
+
+ public static Iterable<BlockPos> withinManhattan(BlockPos center, int rangeX, int rangeY, int rangeZ) {
+ int i = rangeX + rangeY + rangeZ;
+- int j = center.getX();
+- int k = center.getY();
+- int l = center.getZ();
++ // Paper start - rename variables to fix conflict with anonymous class (remap fix)
++ int centerX = center.getX();
++ int centerY = center.getY();
++ int centerZ = center.getZ();
++ // Paper end
+ return () -> new AbstractIterator<BlockPos>() {
+ private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
+ private int currentDepth;
+@@ -340,7 +342,7 @@ public class BlockPos extends Vec3i {
+ protected BlockPos computeNext() {
+ if (this.zMirror) {
+ this.zMirror = false;
+- this.cursor.setZ(l - (this.cursor.getZ() - l));
++ this.cursor.setZ(centerZ - (this.cursor.getZ() - centerZ)); // Paper - remap fix
+ return this.cursor;
+ } else {
+ BlockPos blockPos;
+@@ -366,7 +368,7 @@ public class BlockPos extends Vec3i {
+ int k = this.currentDepth - Math.abs(i) - Math.abs(j);
+ if (k <= rangeZ) {
+ this.zMirror = k != 0;
+- blockPos = this.cursor.set(j + i, k + j, l + k);
++ blockPos = this.cursor.set(centerX + i, centerY + j, centerZ + k); // Paper - remap fix
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
+index 7344cff32fa6fe3dedb74ed98126072c55b0abd2..d98b28e9488a5a7736719cf656736bb026ec8c7e 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
+@@ -169,10 +169,10 @@ public class BehaviorUtils {
+
+ return optional.map((uuid) -> {
+ return ((ServerLevel) entity.level()).getEntity(uuid);
+- }).map((entity) -> {
++ }).map((entity1) -> { // Paper - remap fix
+ LivingEntity entityliving1;
+
+- if (entity instanceof LivingEntity entityliving2) {
++ if (entity1 instanceof LivingEntity entityliving2) { // Paper - remap fix
+ entityliving1 = entityliving2;
+ } else {
+ entityliving1 = null;
+diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
+index f3a2612f0e27c36d5206334307eac1880ce8c4b7..4d4d413b8527e1a109276928611b8c857ad6f6aa 100644
+--- a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
++++ b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
+@@ -259,8 +259,8 @@ public class LootTable {
+
+ public static class Builder implements FunctionUserBuilder<LootTable.Builder> {
+
+- private final Builder<LootPool> pools = ImmutableList.builder();
+- private final Builder<LootItemFunction> functions = ImmutableList.builder();
++ private final ImmutableList.Builder<LootPool> pools = ImmutableList.builder();
++ private final ImmutableList.Builder<LootItemFunction> functions = ImmutableList.builder();
+ private ContextKeySet paramSet;
+ private Optional<ResourceLocation> randomSequence;
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java
+index eaa46bf5954ed7c2be6d4b3772b5f2e971505c78..c101d01b55472efc9fc2829b8c17db5377ed57ff 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java
+@@ -133,7 +133,7 @@ public abstract class CraftBoat extends CraftVehicle implements Boat {
+ throw new EnumConstantNotPresentException(Type.class, boatType.toString());
+ }
+
+- public static Status boatStatusFromNms(net.minecraft.world.entity.vehicle.Boat.EnumStatus enumStatus) {
++ public static Status boatStatusFromNms(net.minecraft.world.entity.vehicle.AbstractBoat.Status enumStatus) { // Paper - remap fixes
+ return switch (enumStatus) {
+ default -> throw new EnumConstantNotPresentException(Status.class, enumStatus.name());
+ case IN_AIR -> Status.IN_AIR;
+diff --git a/src/test/java/org/bukkit/DyeColorsTest.java b/src/test/java/org/bukkit/DyeColorsTest.java
+index e96d821da0698dd42651500fb97a0856a9e9ce02..fb7d40181abdaa5b2ce607db47c09d0d0a19c86d 100644
+--- a/src/test/java/org/bukkit/DyeColorsTest.java
++++ b/src/test/java/org/bukkit/DyeColorsTest.java
+@@ -3,7 +3,6 @@ package org.bukkit;
+ import static org.bukkit.support.MatcherAssert.*;
+ import static org.hamcrest.Matchers.*;
+
+-import net.minecraft.world.item.DyeColor;
+ import org.bukkit.support.environment.Normal;
+ import org.junit.jupiter.params.ParameterizedTest;
+ import org.junit.jupiter.params.provider.EnumSource;
+@@ -15,7 +14,7 @@ public class DyeColorsTest {
+ @EnumSource(DyeColor.class)
+ public void checkColor(DyeColor dye) {
+ Color color = dye.getColor();
+- int nmsColorArray = DyeColor.byId(dye.getWoolData()).getTextureDiffuseColor();
++ int nmsColorArray = net.minecraft.world.item.DyeColor.byId(dye.getWoolData()).getTextureDiffuseColor(); // Paper - remap fix
+ Color nmsColor = Color.fromARGB(nmsColorArray);
+ assertThat(color, is(nmsColor));
+ }
+@@ -24,7 +23,7 @@ public class DyeColorsTest {
+ @EnumSource(org.bukkit.DyeColor.class)
+ public void checkFireworkColor(org.bukkit.DyeColor dye) {
+ Color color = dye.getFireworkColor();
+- int nmsColor = DyeColor.byId(dye.getWoolData()).getFireworkColor();
++ int nmsColor = net.minecraft.world.item.DyeColor.byId(dye.getWoolData()).getFireworkColor(); // Paper - remap fix
+ assertThat(color, is(Color.fromRGB(nmsColor)));
+ }
+ }
+diff --git a/src/test/java/org/bukkit/ParticleTest.java b/src/test/java/org/bukkit/ParticleTest.java
+index dc3f6dc2a4e52449f3d1b2e89900aae0f57dd4bf..c47ce2e5d23af967167fc2f61daa0efe1a85c8c1 100644
+--- a/src/test/java/org/bukkit/ParticleTest.java
++++ b/src/test/java/org/bukkit/ParticleTest.java
+@@ -279,7 +279,7 @@ public class ParticleTest {
+ Check in CraftParticle if the conversion is still correct.
+ """, bukkit.getKey()));
+
+- DataResult<Tag> encoded = assertDoesNotThrow(() -> minecraft.codec().codec().encodeStart(DynamicOpsNBT.INSTANCE, particleParam),
++ DataResult<Tag> encoded = assertDoesNotThrow(() -> minecraft.codec().codec().encodeStart(NbtOps.INSTANCE, particleParam), // Paper - remap fix
+ String.format("""
+ Could not encoded particle param for particle %s.
+ This can indicated, that the wrong particle param is created in CraftParticle.
+diff --git a/src/test/java/org/bukkit/entity/EntityTypesTest.java b/src/test/java/org/bukkit/entity/EntityTypesTest.java
+index 9df52ab0f04758dd04f45f6029fe120ac1a933af..d513d926ddabd61a03172adb846afb7674ed402e 100644
+--- a/src/test/java/org/bukkit/entity/EntityTypesTest.java
++++ b/src/test/java/org/bukkit/entity/EntityTypesTest.java
+@@ -6,7 +6,6 @@ import java.util.Set;
+ import java.util.stream.Collectors;
+ import net.minecraft.core.registries.BuiltInRegistries;
+ import net.minecraft.resources.ResourceLocation;
+-import net.minecraft.world.entity.EntityType;
+ import org.bukkit.support.environment.AllFeatures;
+ import org.junit.jupiter.api.Test;
+
+@@ -17,8 +16,8 @@ public class EntityTypesTest {
+ public void testMaps() {
+ Set<EntityType> allBukkit = Arrays.stream(EntityType.values()).filter((b) -> b.getName() != null).collect(Collectors.toSet());
+
+- for (EntityType<?> nms : BuiltInRegistries.ENTITY_TYPE) {
+- ResourceLocation key = EntityType.getKey(nms);
++ for (net.minecraft.world.entity.EntityType<?> nms : BuiltInRegistries.ENTITY_TYPE) { // Paper - remap fix
++ ResourceLocation key = net.minecraft.world.entity.EntityType.getKey(nms); // Paper - remap fix
+
+ org.bukkit.entity.EntityType bukkit = org.bukkit.entity.EntityType.fromName(key.getPath());
+ assertNotNull(bukkit, "Missing nms->bukkit " + key);
+diff --git a/src/test/java/org/bukkit/entity/PandaGeneTest.java b/src/test/java/org/bukkit/entity/PandaGeneTest.java
+index 4a3ac959f0dcc35f80371443383be1f8b42b6d95..e8520f541fda2d1cd9677f3fc7d7d295743f88b2 100644
+--- a/src/test/java/org/bukkit/entity/PandaGeneTest.java
++++ b/src/test/java/org/bukkit/entity/PandaGeneTest.java
+@@ -2,7 +2,6 @@ package org.bukkit.entity;
+
+ import static org.junit.jupiter.api.Assertions.*;
+
+-import net.minecraft.world.entity.animal.Panda;
+ import org.bukkit.craftbukkit.entity.CraftPanda;
+ import org.bukkit.support.environment.Normal;
+ import org.junit.jupiter.api.Test;
+@@ -12,8 +11,8 @@ public class PandaGeneTest {
+
+ @Test
+ public void testBukkit() {
+- for (Panda.Gene gene : Panda.Gene.values()) {
+- Panda.Gene nms = CraftPanda.toNms(gene);
++ for (Panda.Gene gene : Panda.Gene.values()) { // Paper - remap fix
++ net.minecraft.world.entity.animal.Panda.Gene nms = CraftPanda.toNms(gene); // Paper - remap fix
+
+ assertNotNull(nms, "NMS gene null for " + gene);
+ assertEquals(gene.isRecessive(), nms.isRecessive(), "Recessive status did not match " + gene);
+@@ -23,7 +22,7 @@ public class PandaGeneTest {
+
+ @Test
+ public void testNMS() {
+- for (Panda.Gene gene : Panda.Gene.values()) {
++ for (net.minecraft.world.entity.animal.Panda.Gene gene : net.minecraft.world.entity.animal.Panda.Gene.values()) { // Paper - remap fix
+ org.bukkit.entity.Panda.Gene bukkit = CraftPanda.fromNms(gene);
+
+ assertNotNull(bukkit, "Bukkit gene null for " + gene);
+diff --git a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
+index 9654a6b8d583c9c8842b14298e2f697761bdd705..dbd5b8684d4c86ff5a6f20f53fe30d0b30c384bf 100644
+--- a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
++++ b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
+@@ -31,17 +31,17 @@ public class RegistryConstantsTest {
+
+ @Test
+ public void testTrimMaterial() {
+- this.testExcessConstants(TrimMaterial.class, Registry.TRIM_MATERIAL);
++ this.testExcessConstants(TrimMaterial.class, org.bukkit.Registry.TRIM_MATERIAL); // Paper - remap fix
+ this.testMissingConstants(TrimMaterial.class, Registries.TRIM_MATERIAL);
+ }
+
+ @Test
+ public void testTrimPattern() {
+- this.testExcessConstants(TrimPattern.class, Registry.TRIM_PATTERN);
++ this.testExcessConstants(TrimPattern.class, org.bukkit.Registry.TRIM_PATTERN); // Paper - remap fix
+ this.testMissingConstants(TrimPattern.class, Registries.TRIM_PATTERN);
+ }
+
+- private <T extends Keyed> void testExcessConstants(Class<T> clazz, Registry<T> registry) {
++ private <T extends Keyed> void testExcessConstants(Class<T> clazz, org.bukkit.Registry<T> registry) { // Paper - remap fix
+ List<NamespacedKey> excessKeys = new ArrayList<>();
+
+ for (Field field : clazz.getFields()) {
+diff --git a/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java b/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java
+index 52f057a3b81ae798a8ff354a90e2cc64264c0398..0c704fb9e73229f5632f1f48e59fccc0daf9ec26 100644
+--- a/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java
++++ b/src/test/java/org/bukkit/registry/RegistryLoadOrderTest.java
+@@ -25,7 +25,7 @@ public class RegistryLoadOrderTest {
+
+ private static boolean initInterface = false;
+ private static boolean initAbstract = false;
+- private static Registry<Keyed> registry;
++ private static org.bukkit.Registry<Keyed> registry; // Paper - remap fix
+
+ public static Stream<Arguments> data() {
+ return Stream.of(
diff --git a/patches/server/0003-Build-system-changes.patch b/patches/server/0003-Build-system-changes.patch
new file mode 100644
index 0000000000..9775928a69
--- /dev/null
+++ b/patches/server/0003-Build-system-changes.patch
@@ -0,0 +1,185 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Mon, 29 Feb 2016 20:40:33 -0600
+Subject: [PATCH] Build system changes
+
+== AT ==
+public net.minecraft.server.packs.VanillaPackResourcesBuilder safeGetPath(Ljava/net/URI;)Ljava/nio/file/Path;
+
+Co-authored-by: Jake Potrebic <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 36e6426438865213d2e5e11ee3b1d6e538d475f7..56c201841194bdea8c7d9f07bd105aefb7232697 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -8,9 +8,7 @@ plugins {
+ dependencies {
+ implementation(project(":paper-api"))
+ implementation("jline:jline:2.12.1")
+- implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") {
+- exclude(group = "org.apache.logging.log4j", module = "log4j-api")
+- }
++ implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") // Paper - remove exclusion
+ implementation("org.ow2.asm:asm-commons:9.7.1")
+ implementation("commons-lang:commons-lang:2.6")
+ runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
+@@ -39,6 +37,7 @@ tasks.jar {
+ val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim()
+ val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\""
+ val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper
++ val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper
+ attributes(
+ "Main-Class" to "org.bukkit.craftbukkit.Main",
+ "Implementation-Title" to "CraftBukkit",
+@@ -47,6 +46,9 @@ tasks.jar {
+ "Specification-Title" to "Bukkit",
+ "Specification-Version" to project.version,
+ "Specification-Vendor" to "Bukkit Team",
++ "Git-Branch" to gitBranch, // Paper
++ "Git-Commit" to gitHash, // Paper
++ "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper
+ )
+ for (tld in setOf("net", "com", "org")) {
+ attributes("$tld/bukkit", "Sealed" to true)
+@@ -59,6 +61,17 @@ publishing {
+ }
+ }
+
++// Paper start
++val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks.ScanJarForBadCalls::class) {
++ badAnnotations.add("Lio/papermc/paper/annotation/DoNotUse;")
++ jarToScan.set(tasks.serverJar.flatMap { it.archiveFile })
++ classpath.from(configurations.compileClasspath)
++}
++tasks.check {
++ dependsOn(scanJar)
++}
++// Paper end
++
+ tasks.test {
+ include("**/**TestSuite.class")
+ workingDir = temporaryDir
+@@ -128,4 +141,5 @@ tasks.registerRunTask("runReobf") {
+ tasks.registerRunTask("runDev") {
+ description = "Spin up a non-relocated Mojang-mapped test server"
+ classpath(sourceSets.main.map { it.runtimeClasspath })
++ jvmArgs("-DPaper.pushPaperAssetsRoot=true")
+ }
+diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java
+index bfc8f152fa91dff1dcd5fd07fc067e8e5e480002..262660d115a5d5cbecfbae995955a24283e666b0 100644
+--- a/src/main/java/net/minecraft/resources/ResourceLocation.java
++++ b/src/main/java/net/minecraft/resources/ResourceLocation.java
+@@ -32,6 +32,7 @@ public final class ResourceLocation implements Comparable<ResourceLocation> {
+ public static final char NAMESPACE_SEPARATOR = ':';
+ public static final String DEFAULT_NAMESPACE = "minecraft";
+ public static final String REALMS_NAMESPACE = "realms";
++ public static final String PAPER_NAMESPACE = "paper"; // Paper
+ private final String namespace;
+ private final String path;
+
+diff --git a/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java b/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
+index 14fc03563daea531314c7ceba56dbb47884010ee..fcf95958ef659c7aa8e28026961fa1d6a5f8b28c 100644
+--- a/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
++++ b/src/main/java/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
+@@ -138,6 +138,15 @@ public class VanillaPackResourcesBuilder {
+
+ public VanillaPackResourcesBuilder applyDevelopmentConfig() {
+ developmentConfig.accept(this);
++ if (Boolean.getBoolean("Paper.pushPaperAssetsRoot")) {
++ try {
++ this.pushAssetPath(net.minecraft.server.packs.PackType.SERVER_DATA, net.minecraft.server.packs.VanillaPackResourcesBuilder.safeGetPath(java.util.Objects.requireNonNull(
++ // Important that this is a patched class
++ VanillaPackResourcesBuilder.class.getResource("/data/.paperassetsroot"), "Missing required .paperassetsroot file").toURI()).getParent());
++ } catch (java.net.URISyntaxException | IOException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
+ return this;
+ }
+
+diff --git a/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java b/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java
+index feca36209fd2405fab70f564f63e627b8b78ac18..396ec10a76bdadbf5be2f0e15e88eed47619004d 100644
+--- a/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java
++++ b/src/main/java/net/minecraft/server/packs/repository/ServerPacksSource.java
+@@ -48,7 +48,7 @@ public class ServerPacksSource extends BuiltInPackSource {
+ public static VanillaPackResources createVanillaPackSource() {
+ return new VanillaPackResourcesBuilder()
+ .setMetadata(BUILT_IN_METADATA)
+- .exposeNamespace("minecraft")
++ .exposeNamespace("minecraft", ResourceLocation.PAPER_NAMESPACE) // Paper
+ .applyDevelopmentConfig()
+ .pushJarResources()
+ .build(VANILLA_PACK_INFO);
+@@ -68,7 +68,18 @@ public class ServerPacksSource extends BuiltInPackSource {
+ @Nullable
+ @Override
+ protected Pack createBuiltinPack(String fileName, Pack.ResourcesSupplier packFactory, Component displayName) {
+- return Pack.readMetaAndCreate(createBuiltInPackLocation(fileName, displayName), packFactory, PackType.SERVER_DATA, FEATURE_SELECTION_CONFIG);
++ // Paper start - custom built-in pack
++ final PackLocationInfo info;
++ final PackSelectionConfig packConfig;
++ if ("paper".equals(fileName)) {
++ info = new PackLocationInfo(fileName, displayName, PackSource.BUILT_IN, Optional.empty());
++ packConfig = new PackSelectionConfig(true, Pack.Position.TOP, true);
++ } else {
++ info = createBuiltInPackLocation(fileName, displayName);
++ packConfig = FEATURE_SELECTION_CONFIG;
++ }
++ return Pack.readMetaAndCreate(info, packFactory, PackType.SERVER_DATA, packConfig);
++ // Paper end - custom built-in pack
+ }
+
+ public static PackRepository createPackRepository(Path dataPacksPath, DirectoryValidator symlinkFinder) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index 37b916b97d21c8250725f6756312bf92da0eb487..af267523b60aa9076ac3c8f92ceac65a54ffbb00 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -199,7 +199,7 @@ public class Main {
+ }
+
+ if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) {
+- Date buildDate = new Date(Integer.parseInt(Main.class.getPackage().getImplementationVendor()) * 1000L);
++ Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper
+
+ Calendar deadline = Calendar.getInstance();
+ deadline.add(Calendar.DAY_OF_YEAR, -2);
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
+index 93046379d0cefd5d3236fc59e698809acdc18f80..774556a62eb240da42e84db4502e2ed43495be17 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
+@@ -11,7 +11,7 @@ public final class Versioning {
+ public static String getBukkitVersion() {
+ String result = "Unknown-Version";
+
+- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/org.spigotmc/spigot-api/pom.properties");
++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties");
+ Properties properties = new Properties();
+
+ if (stream != null) {
+diff --git a/src/main/resources/data/.paperassetsroot b/src/main/resources/data/.paperassetsroot
+new file mode 100644
+index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+diff --git a/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta b/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta
+new file mode 100644
+index 0000000000000000000000000000000000000000..288fbe68c6053f40e72f0feedef0ae0fed10fa67
+--- /dev/null
++++ b/src/main/resources/data/minecraft/datapacks/paper/pack.mcmeta
+@@ -0,0 +1,6 @@
++{
++ "pack": {
++ "description": "Built-in Paper Datapack",
++ "pack_format": 41
++ }
++}
+diff --git a/src/test/java/org/bukkit/support/RegistryHelper.java b/src/test/java/org/bukkit/support/RegistryHelper.java
+index f9ed3fd96cb7474785610fe0f87e550456349287..5781c2fab2d407b4a22d5fc80e1c03e907617316 100644
+--- a/src/test/java/org/bukkit/support/RegistryHelper.java
++++ b/src/test/java/org/bukkit/support/RegistryHelper.java
+@@ -70,6 +70,7 @@ public final class RegistryHelper {
+ }
+
+ public static void setup(FeatureFlagSet featureFlagSet) {
++ System.setProperty("Paper.pushPaperAssetsRoot", "true"); // Paper - build system changes - push asset root
+ SharedConstants.tryDetectVersion();
+ Bootstrap.bootStrap();
+
diff --git a/patches/server/0004-Test-changes.patch b/patches/server/0004-Test-changes.patch
new file mode 100644
index 0000000000..44bae88788
--- /dev/null
+++ b/patches/server/0004-Test-changes.patch
@@ -0,0 +1,567 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Mon, 13 Feb 2023 14:14:56 -0800
+Subject: [PATCH] Test changes
+
+- configure mockito agent to address changes in newer java versions see https://openjdk.org/jeps/451
+
+Co-authored-by: yannnicklamprecht <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 56c201841194bdea8c7d9f07bd105aefb7232697..7f1d42ca8df9f72d3d05501f36ae8aefffa6ade7 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -5,6 +5,18 @@ plugins {
+ `maven-publish`
+ }
+
++// Paper start - configure mockito agent that is needed in newer java versions
++val mockitoAgent = configurations.register("mockitoAgent")
++abstract class MockitoAgentProvider : CommandLineArgumentProvider {
++ @get:CompileClasspath
++ abstract val fileCollection: ConfigurableFileCollection
++
++ override fun asArguments(): Iterable<String> {
++ return listOf("-javaagent:" + fileCollection.files.single().absolutePath)
++ }
++}
++// Paper end - configure mockito agent that is needed in newer java versions
++
+ dependencies {
+ implementation(project(":paper-api"))
+ implementation("jline:jline:2.12.1")
+@@ -22,7 +34,9 @@ dependencies {
+ testImplementation("org.junit.platform:junit-platform-suite-engine:1.10.0")
+ testImplementation("org.hamcrest:hamcrest:2.2")
+ testImplementation("org.mockito:mockito-core:5.14.1")
++ mockitoAgent("org.mockito:mockito-core:5.14.1") { isTransitive = false } // Paper - configure mockito agent that is needed in newer java versions
+ testImplementation("org.ow2.asm:asm-tree:9.7.1")
++ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
+ }
+
+ paperweight {
+@@ -56,6 +70,12 @@ tasks.jar {
+ }
+ }
+
++// Paper start - compile tests with -parameters for better junit parameterized test names
++tasks.compileTestJava {
++ options.compilerArgs.add("-parameters")
++}
++// Paper end
++
+ publishing {
+ publications.create<MavenPublication>("maven") {
+ }
+@@ -79,6 +99,11 @@ tasks.test {
+ forkEvery = 1
+ excludeTags("Slow")
+ }
++ // Paper start - configure mockito agent that is needed in newer java versions
++ val provider = objects.newInstance<MockitoAgentProvider>()
++ provider.fileCollection.from(mockitoAgent)
++ jvmArgumentProviders.add(provider)
++ // Paper end - configure mockito agent that is needed in newer java versions
+ }
+
+ fun TaskContainer.registerRunTask(
+diff --git a/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d8857a05858585113bc7efde3416748effb53d01
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java
+@@ -0,0 +1,34 @@
++package io.papermc.paper.registry;
++
++import java.util.Optional;
++import java.util.stream.Stream;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import org.bukkit.support.AbstractTestingBase;
++import org.junit.jupiter.api.BeforeAll;
++import org.junit.jupiter.params.ParameterizedTest;
++import org.junit.jupiter.params.provider.MethodSource;
++
++import static org.junit.jupiter.api.Assertions.assertTrue;
++
++@AllFeatures
++class RegistryKeyTest {
++
++ @BeforeAll
++ static void before() throws ClassNotFoundException {
++ Class.forName(RegistryKey.class.getName()); // load all keys so they are found for the test
++ }
++
++ static Stream<RegistryKey<?>> data() {
++ return RegistryKeyImpl.REGISTRY_KEYS.stream();
++ }
++
++ @ParameterizedTest
++ @MethodSource("data")
++ void testApiRegistryKeysExist(final RegistryKey<?> key) {
++ final Optional<Registry<Object>> registry = RegistryHelper.getRegistry().lookup(ResourceKey.createRegistryKey(ResourceLocation.parse(key.key().asString())));
++ assertTrue(registry.isPresent(), "Missing vanilla registry for " + key.key().asString());
++
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/util/EmptyTag.java b/src/test/java/io/papermc/paper/util/EmptyTag.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6eb95a5e2534974c0e52e2b78b04e7c2b2f28525
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/util/EmptyTag.java
+@@ -0,0 +1,31 @@
++package io.papermc.paper.util;
++
++import java.util.Collections;
++import java.util.Set;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++import org.bukkit.Tag;
++import org.jetbrains.annotations.NotNull;
++
++public record EmptyTag(NamespacedKey key) implements Tag<Keyed> {
++
++ @SuppressWarnings("deprecation")
++ public EmptyTag() {
++ this(NamespacedKey.randomKey());
++ }
++
++ @Override
++ public @NotNull NamespacedKey getKey() {
++ return this.key;
++ }
++
++ @Override
++ public boolean isTagged(@NotNull final Keyed item) {
++ return false;
++ }
++
++ @Override
++ public @NotNull Set<Keyed> getValues() {
++ return Collections.emptySet();
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/util/MethodParameterProvider.java b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3f58ef36df34cd15fcb72189eeff057654adf0c6
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java
+@@ -0,0 +1,206 @@
++/*
++ * Copyright 2015-2023 the original author or authors of https://github.com/junit-team/junit5/blob/6593317c15fb556febbde11914fa7afe00abf8cd/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java
++ *
++ * All rights reserved. This program and the accompanying materials are
++ * made available under the terms of the Eclipse Public License v2.0 which
++ * accompanies this distribution and is available at
++ *
++ * https://www.eclipse.org/legal/epl-v20.html
++ */
++
++package io.papermc.paper.util;
++
++import java.lang.reflect.Method;
++import java.lang.reflect.Parameter;
++import java.util.List;
++import java.util.function.Predicate;
++import java.util.stream.Stream;
++import org.junit.jupiter.api.Test;
++import org.junit.jupiter.api.TestFactory;
++import org.junit.jupiter.api.TestTemplate;
++import org.junit.jupiter.api.extension.ExtensionContext;
++import org.junit.jupiter.params.support.AnnotationConsumer;
++import org.junit.platform.commons.JUnitException;
++import org.junit.platform.commons.PreconditionViolationException;
++import org.junit.platform.commons.util.ClassLoaderUtils;
++import org.junit.platform.commons.util.CollectionUtils;
++import org.junit.platform.commons.util.Preconditions;
++import org.junit.platform.commons.util.ReflectionUtils;
++import org.junit.platform.commons.util.StringUtils;
++import org.junitpioneer.jupiter.cartesian.CartesianParameterArgumentsProvider;
++
++import static java.lang.String.format;
++import static java.util.Arrays.stream;
++import static java.util.stream.Collectors.toList;
++import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated;
++import static org.junit.platform.commons.util.CollectionUtils.isConvertibleToStream;
++
++public class MethodParameterProvider implements CartesianParameterArgumentsProvider<Object>, AnnotationConsumer<MethodParameterSource> {
++ private MethodParameterSource source;
++
++ MethodParameterProvider() {
++ }
++
++ @Override
++ public void accept(final MethodParameterSource source) {
++ this.source = source;
++ }
++
++ @Override
++ public Stream<Object> provideArguments(ExtensionContext context, Parameter parameter) {
++ return this.provideArguments(context, this.source);
++ }
++
++ // Below is mostly copied from MethodArgumentsProvider
++
++ private static final Predicate<Method> isFactoryMethod = //
++ method -> isConvertibleToStream(method.getReturnType()) && !isTestMethod(method);
++
++ protected Stream<Object> provideArguments(ExtensionContext context, MethodParameterSource methodSource) {
++ Class<?> testClass = context.getRequiredTestClass();
++ Method testMethod = context.getRequiredTestMethod();
++ Object testInstance = context.getTestInstance().orElse(null);
++ String[] methodNames = methodSource.value();
++ // @formatter:off
++ return stream(methodNames)
++ .map(factoryMethodName -> findFactoryMethod(testClass, testMethod, factoryMethodName))
++ .map(factoryMethod -> validateFactoryMethod(factoryMethod, testInstance))
++ .map(factoryMethod -> context.getExecutableInvoker().invoke(factoryMethod, testInstance))
++ .flatMap(CollectionUtils::toStream);
++ // @formatter:on
++ }
++
++ private static Method findFactoryMethod(Class<?> testClass, Method testMethod, String factoryMethodName) {
++ String originalFactoryMethodName = factoryMethodName;
++
++ // If the user did not provide a factory method name, find a "default" local
++ // factory method with the same name as the parameterized test method.
++ if (StringUtils.isBlank(factoryMethodName)) {
++ factoryMethodName = testMethod.getName();
++ return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName);
++ }
++
++ // Convert local factory method name to fully-qualified method name.
++ if (!looksLikeAFullyQualifiedMethodName(factoryMethodName)) {
++ factoryMethodName = testClass.getName() + "#" + factoryMethodName;
++ }
++
++ // Find factory method using fully-qualified name.
++ Method factoryMethod = findFactoryMethodByFullyQualifiedName(testClass, testMethod, factoryMethodName);
++
++ // Ensure factory method has a valid return type and is not a test method.
++ Preconditions.condition(isFactoryMethod.test(factoryMethod), () -> format(
++ "Could not find valid factory method [%s] for test class [%s] but found the following invalid candidate: %s",
++ originalFactoryMethodName, testClass.getName(), factoryMethod));
++
++ return factoryMethod;
++ }
++
++ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodName) {
++ if (factoryMethodName.contains("#")) {
++ return true;
++ }
++ int indexOfFirstDot = factoryMethodName.indexOf('.');
++ if (indexOfFirstDot == -1) {
++ return false;
++ }
++ int indexOfLastOpeningParenthesis = factoryMethodName.lastIndexOf('(');
++ if (indexOfLastOpeningParenthesis > 0) {
++ // Exclude simple/local method names with parameters
++ return indexOfFirstDot < indexOfLastOpeningParenthesis;
++ }
++ // If we get this far, we conclude the supplied factory method name "looks"
++ // like it was intended to be a fully-qualified method name, even if the
++ // syntax is invalid. We do this in order to provide better diagnostics for
++ // the user when a fully-qualified method name is in fact invalid.
++ return true;
++ }
++
++ // package-private for testing
++ static Method findFactoryMethodByFullyQualifiedName(
++ Class<?> testClass, Method testMethod,
++ String fullyQualifiedMethodName
++ ) {
++ String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName);
++ String className = methodParts[0];
++ String methodName = methodParts[1];
++ String methodParameters = methodParts[2];
++ ClassLoader classLoader = ClassLoaderUtils.getClassLoader(testClass);
++ Class<?> clazz = loadRequiredClass(className, classLoader);
++
++ // Attempt to find an exact match first.
++ Method factoryMethod = ReflectionUtils.findMethod(clazz, methodName, methodParameters).orElse(null);
++ if (factoryMethod != null) {
++ return factoryMethod;
++ }
++
++ boolean explicitParameterListSpecified = //
++ StringUtils.isNotBlank(methodParameters) || fullyQualifiedMethodName.endsWith("()");
++
++ // If we didn't find an exact match but an explicit parameter list was specified,
++ // that's a user configuration error.
++ Preconditions.condition(!explicitParameterListSpecified,
++ () -> format("Could not find factory method [%s(%s)] in class [%s]", methodName, methodParameters,
++ className));
++
++ // Otherwise, fall back to the same lenient search semantics that are used
++ // to locate a "default" local factory method.
++ return findFactoryMethodBySimpleName(clazz, testMethod, methodName);
++ }
++
++ /**
++ * Find the factory method by searching for all methods in the given {@code clazz}
++ * with the desired {@code factoryMethodName} which have return types that can be
++ * converted to a {@link Stream}, ignoring the {@code testMethod} itself as well
++ * as any {@code @Test}, {@code @TestTemplate}, or {@code @TestFactory} methods
++ * with the same name.
++ *
++ * @return the single factory method matching the search criteria
++ * @throws PreconditionViolationException if the factory method was not found or
++ * multiple competing factory methods with the same name were found
++ */
++ private static Method findFactoryMethodBySimpleName(Class<?> clazz, Method testMethod, String factoryMethodName) {
++ Predicate<Method> isCandidate = candidate -> factoryMethodName.equals(candidate.getName())
++ && !testMethod.equals(candidate);
++ List<Method> candidates = ReflectionUtils.findMethods(clazz, isCandidate);
++
++ List<Method> factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList());
++
++ Preconditions.condition(factoryMethods.size() > 0, () -> {
++ // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps
++ // the specified factory method has an invalid return type or is a test method.
++ // In that case, we report the invalid candidates that were found.
++ if (candidates.size() > 0) {
++ return format(
++ "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s",
++ factoryMethodName, clazz.getName(), candidates);
++ }
++ // Otherwise, report that we didn't find anything.
++ return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName());
++ });
++ Preconditions.condition(factoryMethods.size() == 1,
++ () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(),
++ factoryMethodName, clazz.getName(), factoryMethods));
++ return factoryMethods.get(0);
++ }
++
++ private static boolean isTestMethod(Method candidate) {
++ return isAnnotated(candidate, Test.class) || isAnnotated(candidate, TestTemplate.class)
++ || isAnnotated(candidate, TestFactory.class);
++ }
++
++ private static Class<?> loadRequiredClass(String className, ClassLoader classLoader) {
++ return ReflectionUtils.tryToLoadClass(className, classLoader).getOrThrow(
++ cause -> new JUnitException(format("Could not load class [%s]", className), cause));
++ }
++
++ private static Method validateFactoryMethod(Method factoryMethod, Object testInstance) {
++ Preconditions.condition(
++ factoryMethod.getDeclaringClass().isInstance(testInstance) || ReflectionUtils.isStatic(factoryMethod),
++ () -> format("Method '%s' must be static: local factory methods must be static "
++ + "unless the PER_CLASS @TestInstance lifecycle mode is used; "
++ + "external factory methods must always be static.",
++ factoryMethod.toGenericString()));
++ return factoryMethod;
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/util/MethodParameterSource.java b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6cbf11c898439834cffb99ef84e5df1494356809
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
+@@ -0,0 +1,14 @@
++package io.papermc.paper.util;
++
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource;
++
++@Retention(RetentionPolicy.RUNTIME)
++@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
++@CartesianArgumentsSource(MethodParameterProvider.class)
++public @interface MethodParameterSource {
++ String[] value() default {};
++}
+diff --git a/src/test/java/org/bukkit/ExplosionResultTest.java b/src/test/java/org/bukkit/ExplosionResultTest.java
+index ee5ab15bb0bfeb0ff6fa0d720eeff086d92cb459..7419ea2f68607aad27929a608c402bd4c222f95d 100644
+--- a/src/test/java/org/bukkit/ExplosionResultTest.java
++++ b/src/test/java/org/bukkit/ExplosionResultTest.java
+@@ -5,6 +5,7 @@ import net.minecraft.world.level.Explosion;
+ import org.bukkit.craftbukkit.CraftExplosionResult;
+ import org.junit.jupiter.api.Test;
+
[email protected] // Paper - test changes - missing test suite annotation
+ public class ExplosionResultTest {
+
+ @Test
+diff --git a/src/test/java/org/bukkit/registry/RegistryClassTest.java b/src/test/java/org/bukkit/registry/RegistryClassTest.java
+index 297700b237bbd8d5ff9116919f203c82b4037968..ea3d37f387bdb0dd5ae3fba9231ace31d0cebd64 100644
+--- a/src/test/java/org/bukkit/registry/RegistryClassTest.java
++++ b/src/test/java/org/bukkit/registry/RegistryClassTest.java
+@@ -57,6 +57,7 @@ import org.objectweb.asm.Type;
+ * Note: This test class assumes that feature flags only enable more features and do not disable vanilla ones.
+ */
+ @AllFeatures
[email protected] // Paper - disabled for now as it constructs a second root registry, which is not supported on paper
+ public class RegistryClassTest {
+
+ private static final Map<Class<? extends Keyed>, Data> INIT_DATA = new HashMap<>();
+diff --git a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
+index dbd5b8684d4c86ff5a6f20f53fe30d0b30c384bf..7848c3bb78356f74e7e2cb708308626baa708b1f 100644
+--- a/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
++++ b/src/test/java/org/bukkit/registry/RegistryConstantsTest.java
+@@ -26,7 +26,7 @@ public class RegistryConstantsTest {
+ @Test
+ public void testDamageType() {
+ this.testExcessConstants(DamageType.class, Registry.DAMAGE_TYPE);
+- // this.testMissingConstants(DamageType.class, Registries.DAMAGE_TYPE); // WIND_CHARGE not registered
++ this.testMissingConstants(DamageType.class, Registries.DAMAGE_TYPE); // Paper - re-enable this one
+ }
+
+ @Test
+diff --git a/src/test/java/org/bukkit/support/DummyServerHelper.java b/src/test/java/org/bukkit/support/DummyServerHelper.java
+index a678510df2b999b78d0b4d233fd15bf444c97080..0a6ba289a94468b67d282a199250142e1e86f075 100644
+--- a/src/test/java/org/bukkit/support/DummyServerHelper.java
++++ b/src/test/java/org/bukkit/support/DummyServerHelper.java
+@@ -47,7 +47,7 @@ public final class DummyServerHelper {
+ when(instance.getTag(any(), any(), any())).then(mock -> {
+ String registry = mock.getArgument(0);
+ Class<?> clazz = mock.getArgument(2);
+- MinecraftKey key = CraftNamespacedKey.toMinecraft(mock.getArgument(1));
++ net.minecraft.resources.ResourceLocation key = CraftNamespacedKey.toMinecraft(mock.getArgument(1)); // Paper - address remapping issues
+
+ switch (registry) {
+ case org.bukkit.Tag.REGISTRY_BLOCKS -> {
+@@ -66,24 +66,31 @@ public final class DummyServerHelper {
+ }
+ case org.bukkit.Tag.REGISTRY_FLUIDS -> {
+ Preconditions.checkArgument(clazz == org.bukkit.Fluid.class, "Fluid namespace must have fluid type");
+- TagKey<FluidType> fluidTagKey = TagKey.create(Registries.FLUID, key);
++ TagKey<net.minecraft.world.level.material.Fluid> fluidTagKey = TagKey.create(Registries.FLUID, key); // Paper - address remapping issues
+ if (BuiltInRegistries.FLUID.get(fluidTagKey).isPresent()) {
+ return new CraftFluidTag(BuiltInRegistries.FLUID, fluidTagKey);
+ }
+ }
+ case org.bukkit.Tag.REGISTRY_ENTITY_TYPES -> {
+ Preconditions.checkArgument(clazz == org.bukkit.entity.EntityType.class, "Entity type namespace must have entity type");
+- TagKey<EntityTypes<?>> entityTagKey = TagKey.create(Registries.ENTITY_TYPE, key);
++ TagKey<net.minecraft.world.entity.EntityType<?>> entityTagKey = TagKey.create(Registries.ENTITY_TYPE, key); // Paper - address remapping issues
+ if (BuiltInRegistries.ENTITY_TYPE.get(entityTagKey).isPresent()) {
+ return new CraftEntityTag(BuiltInRegistries.ENTITY_TYPE, entityTagKey);
+ }
+ }
+- default -> throw new IllegalArgumentException();
++ default -> new io.papermc.paper.util.EmptyTag(); // Paper - testing additions
+ }
+
+ return null;
+ });
+
++ // Paper start - testing additions
++ final Thread currentThread = Thread.currentThread();
++ when(instance.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread().equals(currentThread));
++ final org.bukkit.plugin.PluginManager pluginManager = new org.bukkit.plugin.SimplePluginManager(instance, new org.bukkit.command.SimpleCommandMap(instance));
++ when(instance.getPluginManager()).thenReturn(pluginManager);
++ // Paper end - testing additions
++
+ return instance;
+ }
+ }
+diff --git a/src/test/java/org/bukkit/support/RegistryHelper.java b/src/test/java/org/bukkit/support/RegistryHelper.java
+index 5781c2fab2d407b4a22d5fc80e1c03e907617316..68ae998b30bbcacae3604fb6581ceca3da1eda18 100644
+--- a/src/test/java/org/bukkit/support/RegistryHelper.java
++++ b/src/test/java/org/bukkit/support/RegistryHelper.java
+@@ -65,6 +65,11 @@ public final class RegistryHelper {
+ List<HolderLookup.RegistryLookup<?>> list1 = TagLoader.buildUpdatedLookups(iregistrycustom_dimension, list);
+ RegistryAccess.Frozen iregistrycustom_dimension1 = RegistryDataLoader.load((ResourceManager) ireloadableresourcemanager, list1, RegistryDataLoader.WORLDGEN_REGISTRIES);
+ LayeredRegistryAccess<RegistryLayer> layers = layeredregistryaccess.replaceFrom(RegistryLayer.WORLDGEN, iregistrycustom_dimension1);
++ // Paper start - load registry here to ensure bukkit object registry are correctly delayed if needed
++ try {
++ Class.forName("org.bukkit.Registry");
++ } catch (final ClassNotFoundException ignored) {}
++ // Paper end - load registry here to ensure bukkit object registry are correctly delayed if needed
+
+ return layers.compositeAccess().freeze();
+ }
+@@ -82,6 +87,11 @@ public final class RegistryHelper {
+ List<HolderLookup.RegistryLookup<?>> list1 = TagLoader.buildUpdatedLookups(iregistrycustom_dimension, list);
+ RegistryAccess.Frozen iregistrycustom_dimension1 = RegistryDataLoader.load((ResourceManager) ireloadableresourcemanager, list1, RegistryDataLoader.WORLDGEN_REGISTRIES);
+ LayeredRegistryAccess<RegistryLayer> layers = layeredregistryaccess.replaceFrom(RegistryLayer.WORLDGEN, iregistrycustom_dimension1);
++ // Paper start - load registry here to ensure bukkit object registry are correctly delayed if needed
++ try {
++ Class.forName("org.bukkit.Registry");
++ } catch (final ClassNotFoundException ignored) {}
++ // Paper end - load registry here to ensure bukkit object registry are correctly delayed if needed
+ RegistryHelper.registry = layers.compositeAccess().freeze();
+ // Register vanilla pack
+ RegistryHelper.dataPack = ReloadableServerResources.loadResources(ireloadableresourcemanager, layers, list, featureFlagSet, Commands.CommandSelection.DEDICATED, 0, MoreExecutors.directExecutor(), MoreExecutors.directExecutor()).join();
+diff --git a/src/test/java/org/bukkit/support/suite/AllFeaturesTestSuite.java b/src/test/java/org/bukkit/support/suite/AllFeaturesTestSuite.java
+index d78661198815b78d041288eb62076514926428ad..2d268498b545db48efa106d2c7afca7f7b74c76d 100644
+--- a/src/test/java/org/bukkit/support/suite/AllFeaturesTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/AllFeaturesTestSuite.java
+@@ -14,7 +14,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for test which need registry values present, with all feature flags set")
+ @IncludeTags("AllFeatures")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @SelectClasses({RegistryClassTest.class, PerRegistryTest.class, RegistryConversionTest.class}) // Make sure general registry tests are run first
+ @ExcludeClassNamePatterns("org.bukkit.craftbukkit.inventory.ItemStack.*Test")
+ @ConfigurationParameter(key = "TestSuite", value = "AllFeatures")
+diff --git a/src/test/java/org/bukkit/support/suite/BundleFeatureTestSuite.java b/src/test/java/org/bukkit/support/suite/BundleFeatureTestSuite.java
+index 8faaffd16fb05bd3d976b6a63835cfa547ec2445..c1ee709083276acb14b474993800dd4894febc47 100644
+--- a/src/test/java/org/bukkit/support/suite/BundleFeatureTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/BundleFeatureTestSuite.java
+@@ -9,7 +9,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for test which need registry values present, with the bundle feature flag set")
+ @IncludeTags("BundleFeature")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @ConfigurationParameter(key = "TestSuite", value = "BundleFeature")
+ public class BundleFeatureTestSuite {
+ }
+diff --git a/src/test/java/org/bukkit/support/suite/LegacyTestSuite.java b/src/test/java/org/bukkit/support/suite/LegacyTestSuite.java
+index 576c35e086345c96325628cf1a048599f9ed6950..ac3c1c88ce5de4b623d17ab0af11a7d04caec869 100644
+--- a/src/test/java/org/bukkit/support/suite/LegacyTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/LegacyTestSuite.java
+@@ -9,7 +9,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for legacy tests")
+ @IncludeTags("Legacy")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @ConfigurationParameter(key = "TestSuite", value = "Legacy")
+ public class LegacyTestSuite {
+ }
+diff --git a/src/test/java/org/bukkit/support/suite/NormalTestSuite.java b/src/test/java/org/bukkit/support/suite/NormalTestSuite.java
+index 661c49c83b9a81512cf181b50f6353dc76e9f0bc..76f61fb60612160477b7da0b095f1c7e4822d4fb 100644
+--- a/src/test/java/org/bukkit/support/suite/NormalTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/NormalTestSuite.java
+@@ -9,7 +9,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for standalone tests, which don't need any registry values present")
+ @IncludeTags("Normal")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @ConfigurationParameter(key = "TestSuite", value = "Normal")
+ public class NormalTestSuite {
+ }
+diff --git a/src/test/java/org/bukkit/support/suite/SlowTestSuite.java b/src/test/java/org/bukkit/support/suite/SlowTestSuite.java
+index f95ff2e9930f4fd0ff284f714fc39afb6b7789ca..60be4c20101bbae8cf027270ff0e1e138d2fe9d2 100644
+--- a/src/test/java/org/bukkit/support/suite/SlowTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/SlowTestSuite.java
+@@ -9,7 +9,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for slow tests, which don't need to run every time")
+ @IncludeTags("Slow")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @ConfigurationParameter(key = "TestSuite", value = "Slow")
+ public class SlowTestSuite {
+ }
+diff --git a/src/test/java/org/bukkit/support/suite/VanillaFeatureTestSuite.java b/src/test/java/org/bukkit/support/suite/VanillaFeatureTestSuite.java
+index 5ee48e92d2b5134a4ba15802087f6afe58c1cb8d..d0e2eacfcd487e2852eff4b1828031dd3649e41a 100644
+--- a/src/test/java/org/bukkit/support/suite/VanillaFeatureTestSuite.java
++++ b/src/test/java/org/bukkit/support/suite/VanillaFeatureTestSuite.java
+@@ -9,7 +9,7 @@ import org.junit.platform.suite.api.SuiteDisplayName;
+ @Suite(failIfNoTests = false)
+ @SuiteDisplayName("Test suite for test which need vanilla registry values present")
+ @IncludeTags("VanillaFeature")
+-@SelectPackages("org.bukkit")
++@SelectPackages({"org.bukkit", "io.papermc"})
+ @ConfigurationParameter(key = "TestSuite", value = "VanillaFeature")
+ public class VanillaFeatureTestSuite {
+ }
diff --git a/patches/server/0005-Paper-config-files.patch b/patches/server/0005-Paper-config-files.patch
new file mode 100644
index 0000000000..500f4f6294
--- /dev/null
+++ b/patches/server/0005-Paper-config-files.patch
@@ -0,0 +1,5525 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Wed, 8 Jun 2022 22:20:16 -0700
+Subject: [PATCH] Paper config files
+
+== AT ==
+public org.spigotmc.SpigotWorldConfig getBoolean(Ljava/lang/String;Z)Z
+public org.spigotmc.SpigotWorldConfig getDouble(Ljava/lang/String;)D
+public org.spigotmc.SpigotWorldConfig getDouble(Ljava/lang/String;D)D
+public org.spigotmc.SpigotWorldConfig getInt(Ljava/lang/String;)I
+public org.spigotmc.SpigotWorldConfig getInt(Ljava/lang/String;I)I
+public org.spigotmc.SpigotWorldConfig getList(Ljava/lang/String;Ljava/lang/Object;)Ljava/util/List;
+public org.spigotmc.SpigotWorldConfig getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
+public net.minecraft.server.dedicated.DedicatedServerProperties reload(Lnet/minecraft/core/RegistryAccess;Ljava/util/Properties;Ljoptsimple/OptionSet;)Lnet/minecraft/server/dedicated/DedicatedServerProperties;
+public net.minecraft.world.level.NaturalSpawner SPAWNING_CATEGORIES
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 7f1d42ca8df9f72d3d05501f36ae8aefffa6ade7..b4a389d0ef9df8ef49abb7049037e391d491d0c9 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -22,6 +22,7 @@ dependencies {
+ implementation("jline:jline:2.12.1")
+ implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") // Paper - remove exclusion
+ implementation("org.ow2.asm:asm-commons:9.7.1")
++ implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files
+ implementation("commons-lang:commons-lang:2.6")
+ runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
+ runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
+diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ef41cf3a7d1e6f2bfe81e0fb865d2f969bbc77c1
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+@@ -0,0 +1,8 @@
++package com.destroystokyo.paper;
++
++/**
++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib
++ */
++@Deprecated(forRemoval = true)
++public class PaperConfig {
++}
+diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c91f109b4cf64dc1b4ef09f38e1cb8bf5cb2be13
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+@@ -0,0 +1,8 @@
++package com.destroystokyo.paper;
++
++/**
++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib
++ */
++@Deprecated(forRemoval = true)
++public class PaperWorldConfig {
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/Configuration.java b/src/main/java/io/papermc/paper/configuration/Configuration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..817fd26cc3591f9cae0f61f4036dde43c4ed60e8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/Configuration.java
+@@ -0,0 +1,13 @@
++package io.papermc.paper.configuration;
++
++public final class Configuration {
++ public static final String VERSION_FIELD = "_version";
++ @Deprecated
++ public static final String LEGACY_CONFIG_VERSION_FIELD = "config-version";
++
++ @Deprecated
++ public static final int FINAL_LEGACY_VERSION = 27;
++
++ private Configuration() {
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..227039a6c69c4c99bbd9c674b3aab0ef5e2c1374
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java
+@@ -0,0 +1,27 @@
++package io.papermc.paper.configuration;
++
++import java.nio.file.Path;
++import org.spongepowered.configurate.loader.HeaderMode;
++import org.spongepowered.configurate.util.MapFactories;
++import org.spongepowered.configurate.yaml.NodeStyle;
++import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
++
++public final class ConfigurationLoaders {
++ private ConfigurationLoaders() {
++ }
++
++ public static YamlConfigurationLoader.Builder naturallySorted() {
++ return YamlConfigurationLoader.builder()
++ .indent(2)
++ .nodeStyle(NodeStyle.BLOCK)
++ .headerMode(HeaderMode.PRESET)
++ .defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural()));
++ }
++
++ public static YamlConfigurationLoader naturallySortedWithoutHeader(final Path path) {
++ return naturallySorted()
++ .headerMode(HeaderMode.NONE)
++ .path(path)
++ .build();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..042478cf7ce150f1f1bc5cddd7fa40f86ec773dd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java
+@@ -0,0 +1,7 @@
++package io.papermc.paper.configuration;
++
++/**
++ * Marker interface for unique sections of a configuration.
++ */
++public abstract class ConfigurationPart {
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..007e01d329a31acf7f4ed4c6dc4de7ad54ccad04
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/Configurations.java
+@@ -0,0 +1,358 @@
++package io.papermc.paper.configuration;
++
++import com.google.common.base.Preconditions;
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.configuration.constraint.Constraint;
++import io.papermc.paper.configuration.constraint.Constraints;
++import java.io.IOException;
++import java.lang.reflect.Type;
++import java.nio.file.AccessDeniedException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.NoSuchElementException;
++import java.util.Objects;
++import java.util.function.UnaryOperator;
++import net.minecraft.core.RegistryAccess;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.GameRules;
++import org.jetbrains.annotations.MustBeInvokedByOverriders;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.CommentedConfigurationNode;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.objectmapping.ObjectMapper;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.util.CheckedFunction;
++import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
++
++public abstract class Configurations<G, W> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ public static final String WORLD_DEFAULTS = "__world_defaults__";
++ public static final ResourceLocation WORLD_DEFAULTS_KEY = ResourceLocation.fromNamespaceAndPath("configurations", WORLD_DEFAULTS);
++ protected final Path globalFolder;
++ protected final Class<G> globalConfigClass;
++ protected final Class<W> worldConfigClass;
++ protected final String globalConfigFileName;
++ protected final String defaultWorldConfigFileName;
++ protected final String worldConfigFileName;
++
++ public Configurations(
++ final Path globalFolder,
++ final Class<G> globalConfigType,
++ final Class<W> worldConfigClass,
++ final String globalConfigFileName,
++ final String defaultWorldConfigFileName,
++ final String worldConfigFileName
++ ) {
++ this.globalFolder = globalFolder;
++ this.globalConfigClass = globalConfigType;
++ this.worldConfigClass = worldConfigClass;
++ this.globalConfigFileName = globalConfigFileName;
++ this.defaultWorldConfigFileName = defaultWorldConfigFileName;
++ this.worldConfigFileName = worldConfigFileName;
++ }
++
++ protected ObjectMapper.Factory.Builder createObjectMapper() {
++ return ObjectMapper.factoryBuilder()
++ .addConstraint(Constraint.class, new Constraint.Factory())
++ .addConstraint(Constraints.Min.class, Number.class, new Constraints.Min.Factory());
++ }
++
++ protected YamlConfigurationLoader.Builder createLoaderBuilder() {
++ return ConfigurationLoaders.naturallySorted();
++ }
++
++ protected abstract boolean isConfigType(final Type type);
++
++ protected abstract int globalConfigVersion();
++
++ protected abstract int worldConfigVersion();
++
++ protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
++ return this.createObjectMapper();
++ }
++
++ @MustBeInvokedByOverriders
++ protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
++ return this.createLoaderBuilder();
++ }
++
++ static <T> CheckedFunction<ConfigurationNode, T, SerializationException> creator(final Class<? extends T> type, final boolean refreshNode) {
++ return node -> {
++ final T instance = node.require(type);
++ if (refreshNode) {
++ node.set(type, instance);
++ }
++ return instance;
++ };
++ }
++
++ static <T> CheckedFunction<ConfigurationNode, T, SerializationException> reloader(Class<T> type, T instance) {
++ return node -> {
++ ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type));
++ ObjectMapper.Mutable<T> mutable = (ObjectMapper.Mutable<T>) factory.get(type);
++ mutable.load(instance, node);
++ return instance;
++ };
++ }
++
++ public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
++ return this.initializeGlobalConfiguration(creator(this.globalConfigClass, true));
++ }
++
++ private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException {
++ try {
++ loader.save(node);
++ } catch (ConfigurateException ex) {
++ if (ex.getCause() instanceof AccessDeniedException) {
++ LOGGER.warn("Could not save {}: Paper could not persist the full set of configuration settings in the configuration file. Any setting missing from the configuration file will be set with its default value in memory. Admins should make sure to review the configuration documentation at https://docs.papermc.io/paper/configuration for more details.", filename, ex);
++ } else throw ex;
++ }
++ }
++
++ protected G initializeGlobalConfiguration(final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
++ final Path configFile = this.globalFolder.resolve(this.globalConfigFileName);
++ final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder()
++ .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build()))
++ .path(configFile)
++ .build();
++ final ConfigurationNode node;
++ if (Files.notExists(configFile)) {
++ node = CommentedConfigurationNode.root(loader.defaultOptions());
++ node.node(Configuration.VERSION_FIELD).raw(this.globalConfigVersion());
++ } else {
++ node = loader.load();
++ this.verifyGlobalConfigVersion(node);
++ }
++ this.applyGlobalConfigTransformations(node);
++ final G instance = creator.apply(node);
++ trySaveFileNode(loader, node, configFile.toString());
++ return instance;
++ }
++
++ protected void verifyGlobalConfigVersion(final ConfigurationNode globalNode) {
++ final ConfigurationNode version = globalNode.node(Configuration.VERSION_FIELD);
++ if (version.virtual()) {
++ LOGGER.warn("The global config file didn't have a version set, assuming latest");
++ version.raw(this.globalConfigVersion());
++ } else if (version.getInt() > this.globalConfigVersion()) {
++ LOGGER.error("Loading a newer configuration than is supported ({} > {})! You may have to backup & delete your global config file to start the server.", version.getInt(), this.globalConfigVersion());
++ }
++ }
++
++ protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException {
++ }
++
++ @MustBeInvokedByOverriders
++ protected ContextMap.Builder createDefaultContextMap(final RegistryAccess registryAccess) {
++ return ContextMap.builder()
++ .put(WORLD_NAME, WORLD_DEFAULTS)
++ .put(WORLD_KEY, WORLD_DEFAULTS_KEY)
++ .put(REGISTRY_ACCESS, registryAccess);
++ }
++
++ public void initializeWorldDefaultsConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
++ final ContextMap contextMap = this.createDefaultContextMap(registryAccess)
++ .put(FIRST_DEFAULT)
++ .build();
++ final Path configFile = this.globalFolder.resolve(this.defaultWorldConfigFileName);
++ final DefaultWorldLoader result = this.createDefaultWorldLoader(false, contextMap, configFile);
++ final YamlConfigurationLoader loader = result.loader();
++ final ConfigurationNode node = loader.load();
++ if (result.isNewFile()) { // add version to new files
++ node.node(Configuration.VERSION_FIELD).raw(this.worldConfigVersion());
++ } else {
++ this.verifyWorldConfigVersion(contextMap, node);
++ }
++ this.applyWorldConfigTransformations(contextMap, node, null);
++ final W instance = node.require(this.worldConfigClass);
++ node.set(this.worldConfigClass, instance);
++ this.trySaveFileNode(loader, node, configFile.toString());
++ }
++
++ private DefaultWorldLoader createDefaultWorldLoader(final boolean requireFile, final ContextMap contextMap, final Path configFile) {
++ boolean willCreate = Files.notExists(configFile);
++ if (requireFile && willCreate) {
++ throw new IllegalStateException("World defaults configuration file '" + configFile + "' doesn't exist");
++ }
++ return new DefaultWorldLoader(
++ this.createWorldConfigLoaderBuilder(contextMap)
++ .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build()))
++ .path(configFile)
++ .build(),
++ willCreate
++ );
++ }
++
++ private record DefaultWorldLoader(YamlConfigurationLoader loader, boolean isNewFile) {
++ }
++
++ protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) {
++ return this.createObjectMapper();
++ }
++
++ @MustBeInvokedByOverriders
++ protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) {
++ return this.createLoaderBuilder();
++ }
++
++ // Make sure to run version transforms on the default world config first via #setupWorldDefaultsConfig
++ public W createWorldConfig(final ContextMap contextMap) throws IOException {
++ return this.createWorldConfig(contextMap, creator(this.worldConfigClass, false));
++ }
++
++ protected W createWorldConfig(final ContextMap contextMap, final CheckedFunction<ConfigurationNode, W, SerializationException> creator) throws IOException {
++ Preconditions.checkArgument(!contextMap.isDefaultWorldContext(), "cannot create world map with default world context");
++ final Path defaultsConfigFile = this.globalFolder.resolve(this.defaultWorldConfigFileName);
++ final YamlConfigurationLoader defaultsLoader = this.createDefaultWorldLoader(true, this.createDefaultContextMap(contextMap.require(REGISTRY_ACCESS)).build(), defaultsConfigFile).loader();
++ final ConfigurationNode defaultsNode = defaultsLoader.load();
++
++ boolean newFile = false;
++ final Path dir = contextMap.require(WORLD_DIRECTORY);
++ final Path worldConfigFile = dir.resolve(this.worldConfigFileName);
++ if (Files.notExists(worldConfigFile)) {
++ PaperConfigurations.createDirectoriesSymlinkAware(dir);
++ Files.createFile(worldConfigFile); // create empty file as template
++ newFile = true;
++ }
++
++ final YamlConfigurationLoader worldLoader = this.createWorldConfigLoaderBuilder(contextMap)
++ .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build()))
++ .path(worldConfigFile)
++ .build();
++ final ConfigurationNode worldNode = worldLoader.load();
++ if (newFile) { // set the version field if new file
++ worldNode.node(Configuration.VERSION_FIELD).set(this.worldConfigVersion());
++ } else {
++ this.verifyWorldConfigVersion(contextMap, worldNode);
++ }
++ this.applyWorldConfigTransformations(contextMap, worldNode, defaultsNode);
++ this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode);
++ this.trySaveFileNode(worldLoader, worldNode, worldConfigFile.toString()); // save before loading node NOTE: don't save the backing node after loading it, or you'll fill up the world-specific config
++ worldNode.mergeFrom(defaultsNode);
++ return creator.apply(worldNode);
++ }
++
++ protected void verifyWorldConfigVersion(final ContextMap contextMap, final ConfigurationNode worldNode) {
++ final ConfigurationNode version = worldNode.node(Configuration.VERSION_FIELD);
++ final String worldName = contextMap.require(WORLD_NAME);
++ if (version.virtual()) {
++ if (worldName.equals(WORLD_DEFAULTS)) {
++ LOGGER.warn("The world defaults config file didn't have a version set, assuming latest");
++ } else {
++ LOGGER.warn("The world config file for " + worldName + " didn't have a version set, assuming latest");
++ }
++ version.raw(this.worldConfigVersion());
++ } else if (version.getInt() > this.worldConfigVersion()) {
++ String msg = "Loading a newer configuration than is supported ({} > {})! ";
++ if (worldName.equals(WORLD_DEFAULTS)) {
++ msg += "You may have to backup & delete the world defaults config file to start the server.";
++ } else {
++ msg += "You may have to backup & delete the " + worldName + " config file to start the server.";
++ }
++ LOGGER.error(msg, version.getInt(), this.worldConfigVersion());
++ }
++ }
++
++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException {
++ }
++
++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException {
++ }
++
++ private UnaryOperator<ConfigurationOptions> applyObjectMapperFactory(final ObjectMapper.Factory factory) {
++ return options -> options.serializers(builder -> builder
++ .register(this::isConfigType, factory.asTypeSerializer())
++ .registerAnnotatedObjects(factory));
++ }
++
++ public Path getWorldConfigFile(ServerLevel level) {
++ return level.convertable.levelDirectory.path().resolve(this.worldConfigFileName);
++ }
++
++ public static class ContextMap {
++ private static final Object VOID = new Object();
++
++ public static Builder builder() {
++ return new Builder();
++ }
++
++ private final Map<ContextKey<?>, Object> backingMap;
++
++ private ContextMap(Map<ContextKey<?>, Object> map) {
++ this.backingMap = Map.copyOf(map);
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T> T require(ContextKey<T> key) {
++ final @Nullable Object value = this.backingMap.get(key);
++ if (value == null) {
++ throw new NoSuchElementException("No element found for " + key + " with type " + key.type());
++ } else if (value == VOID) {
++ throw new IllegalArgumentException("Cannot get the value of a Void key");
++ }
++ return (T) value;
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T> @Nullable T get(ContextKey<T> key) {
++ return (T) this.backingMap.get(key);
++ }
++
++ public boolean has(ContextKey<?> key) {
++ return this.backingMap.containsKey(key);
++ }
++
++ public boolean isDefaultWorldContext() {
++ return this.require(WORLD_KEY).equals(WORLD_DEFAULTS_KEY);
++ }
++
++ public static class Builder {
++
++ private Builder() {
++ }
++
++ private final Map<ContextKey<?>, Object> buildingMap = new HashMap<>();
++
++ public <T> Builder put(ContextKey<T> key, T value) {
++ this.buildingMap.put(key, value);
++ return this;
++ }
++
++ public Builder put(ContextKey<Void> key) {
++ this.buildingMap.put(key, VOID);
++ return this;
++ }
++
++ public ContextMap build() {
++ return new ContextMap(this.buildingMap);
++ }
++ }
++ }
++
++ public static final ContextKey<Path> WORLD_DIRECTORY = new ContextKey<>(Path.class, "world directory");
++ public static final ContextKey<String> WORLD_NAME = new ContextKey<>(String.class, "world name"); // TODO remove when we deprecate level names
++ public static final ContextKey<ResourceLocation> WORLD_KEY = new ContextKey<>(ResourceLocation.class, "world key");
++ public static final ContextKey<Void> FIRST_DEFAULT = new ContextKey<>(Void.class, "first default");
++ public static final ContextKey<RegistryAccess> REGISTRY_ACCESS = new ContextKey<>(RegistryAccess.class, "registry access");
++ public static final ContextKey<GameRules> GAME_RULES = new ContextKey<>(GameRules.class, "game rules");
++
++ public record ContextKey<T>(TypeToken<T> type, String name) {
++
++ public ContextKey(Class<T> type, String name) {
++ this(TypeToken.get(type), name);
++ }
++
++ @Override
++ public String toString() {
++ return "ContextKey{" + this.name + "}";
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..87da4ff63294735bfcbfa8442fb8ae7196b0f197
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+@@ -0,0 +1,328 @@
++package io.papermc.paper.configuration;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.configuration.constraint.Constraints;
++import io.papermc.paper.configuration.type.number.DoubleOr;
++import io.papermc.paper.configuration.type.number.IntOr;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.minecraft.network.protocol.Packet;
++import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.meta.Comment;
++import org.spongepowered.configurate.objectmapping.meta.PostProcess;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++import org.spongepowered.configurate.objectmapping.meta.Setting;
++
++import java.util.Map;
++import java.util.Objects;
++import java.util.OptionalInt;
++
++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
++public class GlobalConfiguration extends ConfigurationPart {
++ private static final Logger LOGGER = LogUtils.getLogger();
++ static final int CURRENT_VERSION = 29; // (when you change the version, change the comment, so it conflicts on rebases): <insert changes here>
++ private static GlobalConfiguration instance;
++ public static GlobalConfiguration get() {
++ return instance;
++ }
++
++ public ChunkLoadingBasic chunkLoadingBasic;
++
++ public class ChunkLoadingBasic extends ConfigurationPart {
++ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
++ public double playerMaxChunkSendRate = 75.0;
++
++ @Comment(
++ "The maximum rate at which chunks will load for any individual player. " +
++ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
++ "chunk is already generated. Set to -1 to disable this limit."
++ )
++ public double playerMaxChunkLoadRate = 100.0;
++
++ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
++ public double playerMaxChunkGenerateRate = -1.0;
++ }
++
++ public ChunkLoadingAdvanced chunkLoadingAdvanced;
++
++ public class ChunkLoadingAdvanced extends ConfigurationPart {
++ @Comment(
++ "Set to true if the server will match the chunk send radius that clients have configured" +
++ "in their view distance settings if the client is less-than the server's send distance."
++ )
++ public boolean autoConfigSendDistance = true;
++
++ @Comment(
++ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
++ )
++ public int playerMaxConcurrentChunkLoads = 0;
++
++ @Comment(
++ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
++ )
++ public int playerMaxConcurrentChunkGenerates = 0;
++ }
++ static void set(GlobalConfiguration instance) {
++ GlobalConfiguration.instance = instance;
++ }
++
++ @Setting(Configuration.VERSION_FIELD)
++ public int version = CURRENT_VERSION;
++
++ public Messages messages;
++
++ public class Messages extends ConfigurationPart {
++ public Kick kick;
++
++ public class Kick extends ConfigurationPart {
++ public Component authenticationServersDown = Component.translatable("multiplayer.disconnect.authservers_down");
++ public Component connectionThrottle = Component.text("Connection throttled! Please wait before reconnecting.");
++ public Component flyingPlayer = Component.translatable("multiplayer.disconnect.flying");
++ public Component flyingVehicle = Component.translatable("multiplayer.disconnect.flying");
++ }
++
++ public Component noPermission = Component.text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", NamedTextColor.RED);
++ public boolean useDisplayNameInQuitMessage = false;
++ }
++
++ public Spark spark;
++
++ public class Spark extends ConfigurationPart {
++ public boolean enabled = true;
++ public boolean enableImmediately = false;
++ }
++
++ public Proxies proxies;
++
++ public class Proxies extends ConfigurationPart {
++ public BungeeCord bungeeCord;
++
++ public class BungeeCord extends ConfigurationPart {
++ public boolean onlineMode = true;
++ }
++
++ public Velocity velocity;
++
++ public class Velocity extends ConfigurationPart {
++ public boolean enabled = false;
++ public boolean onlineMode = true;
++ public String secret = "";
++
++ @PostProcess
++ private void postProcess() {
++ if (!this.enabled) return;
++
++ final String environmentSourcedVelocitySecret = System.getenv("PAPER_VELOCITY_SECRET");
++ if (environmentSourcedVelocitySecret != null && !environmentSourcedVelocitySecret.isEmpty()) {
++ this.secret = environmentSourcedVelocitySecret;
++ }
++
++ if (this.secret.isEmpty()) {
++ LOGGER.error("Velocity is enabled, but no secret key was specified. A secret key is required. Disabling velocity...");
++ this.enabled = false;
++ }
++ }
++ }
++ public boolean proxyProtocol = false;
++ public boolean isProxyOnlineMode() {
++ return org.bukkit.Bukkit.getOnlineMode() || (org.spigotmc.SpigotConfig.bungee && this.bungeeCord.onlineMode) || (this.velocity.enabled && this.velocity.onlineMode);
++ }
++ }
++
++ public Console console;
++
++ public class Console extends ConfigurationPart {
++ public boolean enableBrigadierHighlighting = true;
++ public boolean enableBrigadierCompletions = true;
++ public boolean hasAllPermissions = false;
++ }
++
++ public Watchdog watchdog;
++
++ public class Watchdog extends ConfigurationPart {
++ public int earlyWarningEvery = 5000;
++ public int earlyWarningDelay = 10000;
++ }
++
++ public SpamLimiter spamLimiter;
++
++ public class SpamLimiter extends ConfigurationPart {
++ public int tabSpamIncrement = 1;
++ public int tabSpamLimit = 500;
++ public int recipeSpamIncrement = 1;
++ public int recipeSpamLimit = 20;
++ public int incomingPacketThreshold = 300;
++ }
++
++ public UnsupportedSettings unsupportedSettings;
++
++ public class UnsupportedSettings extends ConfigurationPart {
++ @Comment("This setting allows for exploits related to end portals, for example sand duping")
++ public boolean allowUnsafeEndPortalTeleportation = false;
++ @Comment("This setting controls if players should be able to break bedrock, end portals and other intended to be permanent blocks.")
++ public boolean allowPermanentBlockBreakExploits = false;
++ @Comment("This setting controls if player should be able to use TNT duplication, but this also allows duplicating carpet, rails and potentially other items")
++ public boolean allowPistonDuplication = false;
++ public boolean performUsernameValidation = true;
++ @Comment("This setting controls if players should be able to create headless pistons.")
++ public boolean allowHeadlessPistons = false;
++ @Comment("This setting controls if the vanilla damage tick should be skipped if damage was blocked via a shield.")
++ public boolean skipVanillaDamageTickWhenShieldBlocked = false;
++ @Comment("This setting controls what compression format is used for region files.")
++ public CompressionFormat compressionFormat = CompressionFormat.ZLIB;
++
++ public enum CompressionFormat {
++ GZIP,
++ ZLIB,
++ NONE
++ }
++ }
++
++ public Commands commands;
++
++ public class Commands extends ConfigurationPart {
++ public boolean suggestPlayerNamesWhenNullTabCompletions = true;
++ public boolean fixTargetSelectorTagCompletion = true;
++ public boolean timeCommandAffectsAllWorlds = false;
++ }
++
++ public Logging logging;
++
++ public class Logging extends ConfigurationPart {
++ public boolean deobfuscateStacktraces = true;
++ }
++
++ public Scoreboards scoreboards;
++
++ public class Scoreboards extends ConfigurationPart {
++ public boolean trackPluginScoreboards = false;
++ public boolean saveEmptyScoreboardTeams = true;
++ }
++
++ @SuppressWarnings("unused") // used in postProcess
++ public ChunkSystem chunkSystem;
++
++ public class ChunkSystem extends ConfigurationPart {
++
++ public int ioThreads = -1;
++ public int workerThreads = -1;
++ public String genParallelism = "default";
++
++ @PostProcess
++ private void postProcess() {
++
++ }
++ }
++
++ public ItemValidation itemValidation;
++
++ public class ItemValidation extends ConfigurationPart {
++ public int displayName = 8192;
++ public int loreLine = 8192;
++ public Book book;
++
++ public class Book extends ConfigurationPart {
++ public int title = 8192;
++ public int author = 8192;
++ public int page = 16384;
++ }
++
++ public BookSize bookSize;
++
++ public class BookSize extends ConfigurationPart {
++ public IntOr.Disabled pageMax = new IntOr.Disabled(OptionalInt.of(2560)); // TODO this appears to be a duplicate setting with one above
++ public double totalMultiplier = 0.98D; // TODO this should probably be merged into the above inner class
++ }
++ public boolean resolveSelectorsInBooks = false;
++ }
++
++ public PacketLimiter packetLimiter;
++
++ public class PacketLimiter extends ConfigurationPart {
++ public Component kickMessage = Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED);
++ public PacketLimit allPackets = new PacketLimit(7.0, 500.0, PacketLimit.ViolateAction.KICK);
++ public Map<Class<? extends Packet<?>>, PacketLimit> overrides = Map.of(ServerboundPlaceRecipePacket.class, new PacketLimit(4.0, 5.0, PacketLimit.ViolateAction.DROP));
++
++ @ConfigSerializable
++ public record PacketLimit(@Required double interval, @Required double maxPacketRate, ViolateAction action) {
++ public PacketLimit(final double interval, final double maxPacketRate, final @Nullable ViolateAction action) {
++ this.interval = interval;
++ this.maxPacketRate = maxPacketRate;
++ this.action = Objects.requireNonNullElse(action, ViolateAction.KICK);
++ }
++
++ public boolean isEnabled() {
++ return this.interval > 0.0 && this.maxPacketRate > 0.0;
++ }
++
++ public enum ViolateAction {
++ KICK,
++ DROP;
++ }
++ }
++ }
++
++ public Collisions collisions;
++
++ public class Collisions extends ConfigurationPart {
++ public boolean enablePlayerCollisions = true;
++ public boolean sendFullPosForHardCollidingEntities = true;
++ }
++
++ public PlayerAutoSave playerAutoSave;
++
++
++ public class PlayerAutoSave extends ConfigurationPart {
++ public int rate = -1;
++ private int maxPerTick = -1;
++ public int maxPerTick() {
++ if (this.maxPerTick < 0) {
++ return (this.rate == 1 || this.rate > 100) ? 10 : 20;
++ }
++ return this.maxPerTick;
++ }
++ }
++
++ public Misc misc;
++
++ public class Misc extends ConfigurationPart {
++
++ @SuppressWarnings("unused") // used in postProcess
++ public ChatThreads chatThreads;
++ public class ChatThreads extends ConfigurationPart {
++ private int chatExecutorCoreSize = -1;
++ private int chatExecutorMaxSize = -1;
++
++ @PostProcess
++ private void postProcess() {
++ // TODO: fill in separate patch
++ }
++ }
++ public int maxJoinsPerTick = 5;
++ public boolean fixEntityPositionDesync = true;
++ public boolean loadPermissionsYmlBeforePlugins = true;
++ @Constraints.Min(4)
++ public int regionFileCacheSize = 256;
++ @Comment("See https://luckformula.emc.gs")
++ public boolean useAlternativeLuckFormula = false;
++ public boolean useDimensionTypeForCustomSpawners = false;
++ public boolean strictAdvancementDimensionCheck = false;
++ public IntOr.Default compressionLevel = IntOr.Default.USE_DEFAULT;
++ @Comment("Defines the leniency distance added on the server to the interaction range of a player when validating interact packets.")
++ public DoubleOr.Default clientInteractionLeniencyDistance = DoubleOr.Default.USE_DEFAULT;
++ }
++
++ public BlockUpdates blockUpdates;
++
++ public class BlockUpdates extends ConfigurationPart {
++ public boolean disableNoteblockUpdates = false;
++ public boolean disableTripwireUpdates = false;
++ public boolean disableChorusPlantUpdates = false;
++ public boolean disableMushroomBlockUpdates = false;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b8c42cc2624f325dc8725ebab68bbff0addb3855
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/NestedSetting.java
+@@ -0,0 +1,31 @@
++package io.papermc.paper.configuration;
++
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.AnnotatedElement;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++
++@Documented
++@Retention(RetentionPolicy.RUNTIME)
++@Target(ElementType.FIELD)
++public @interface NestedSetting {
++
++ String[] value();
++
++ class Factory implements NodeResolver.Factory {
++ @Override
++ public @Nullable NodeResolver make(String name, AnnotatedElement element) {
++ if (element.isAnnotationPresent(NestedSetting.class)) {
++ Object[] path = element.getAnnotation(NestedSetting.class).value();
++ if (path.length > 0) {
++ return node -> node.node(path);
++ }
++ }
++ return null;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c5644d8d64f12073e39bc6ed79c8714f4560ff89
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
+@@ -0,0 +1,470 @@
++package io.papermc.paper.configuration;
++
++import com.google.common.base.Suppliers;
++import com.google.common.collect.Table;
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization;
++import io.papermc.paper.configuration.mapping.InnerClassFieldDiscoverer;
++import io.papermc.paper.configuration.serializer.ComponentSerializer;
++import io.papermc.paper.configuration.serializer.EnumValueSerializer;
++import io.papermc.paper.configuration.serializer.NbtPathSerializer;
++import io.papermc.paper.configuration.serializer.PacketClassSerializer;
++import io.papermc.paper.configuration.serializer.StringRepresentableSerializer;
++import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer;
++import io.papermc.paper.configuration.serializer.collections.MapSerializer;
++import io.papermc.paper.configuration.serializer.collections.TableSerializer;
++import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer;
++import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer;
++import io.papermc.paper.configuration.transformation.Transformations;
++import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig;
++import io.papermc.paper.configuration.transformation.global.versioned.V29_LogIPs;
++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration;
++import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig;
++import io.papermc.paper.configuration.transformation.world.versioned.V29_ZeroWorldHeight;
++import io.papermc.paper.configuration.transformation.world.versioned.V30_RenameFilterNbtFromSpawnEgg;
++import io.papermc.paper.configuration.transformation.world.versioned.V31_SpawnLoadedRangeToGameRule;
++import io.papermc.paper.configuration.type.BooleanOrDefault;
++import io.papermc.paper.configuration.type.DespawnRange;
++import io.papermc.paper.configuration.type.Duration;
++import io.papermc.paper.configuration.type.DurationOrDisabled;
++import io.papermc.paper.configuration.type.EngineMode;
++import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer;
++import io.papermc.paper.configuration.type.number.DoubleOr;
++import io.papermc.paper.configuration.type.number.IntOr;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2LongMap;
++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
++import java.io.File;
++import java.io.IOException;
++import java.lang.reflect.Type;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.StandardCopyOption;
++import java.util.List;
++import java.util.function.Function;
++import java.util.function.Supplier;
++import net.minecraft.core.RegistryAccess;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.item.Item;
++import net.minecraft.world.level.GameRules;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
++import org.apache.commons.lang3.RandomStringUtils;
++import org.bukkit.configuration.ConfigurationSection;
++import org.bukkit.configuration.file.YamlConfiguration;
++import org.jetbrains.annotations.VisibleForTesting;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spigotmc.SpigotConfig;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.BasicConfigurationNode;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.objectmapping.ObjectMapper;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
++
++import static com.google.common.base.Preconditions.checkState;
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++@SuppressWarnings("Convert2Diamond")
++public class PaperConfigurations extends Configurations<GlobalConfiguration, WorldConfiguration> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ static final String GLOBAL_CONFIG_FILE_NAME = "paper-global.yml";
++ static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "paper-world-defaults.yml";
++ static final String WORLD_CONFIG_FILE_NAME = "paper-world.yml";
++ public static final String CONFIG_DIR = "config";
++ private static final String BACKUP_DIR ="legacy-backup";
++
++ private static final String GLOBAL_HEADER = String.format("""
++ This is the global configuration file for Paper.
++ As you can see, there's a lot to configure. Some options may impact gameplay, so use
++ with caution, and make sure you know what each option does before configuring.
++
++ If you need help with the configuration or have any questions related to Paper,
++ join us in our Discord or check the docs page.
++
++ The world configuration options have been moved inside
++ their respective world folder. The files are named %s
++
++ Docs: https://docs.papermc.io/
++ Discord: https://discord.gg/papermc
++ Website: https://papermc.io/""", WORLD_CONFIG_FILE_NAME);
++
++ private static final String WORLD_DEFAULTS_HEADER = """
++ This is the world defaults configuration file for Paper.
++ As you can see, there's a lot to configure. Some options may impact gameplay, so use
++ with caution, and make sure you know what each option does before configuring.
++
++ If you need help with the configuration or have any questions related to Paper,
++ join us in our Discord or check the docs page.
++
++ Configuration options here apply to all worlds, unless you specify overrides inside
++ the world-specific config file inside each world folder.
++
++ Docs: https://docs.papermc.io/
++ Discord: https://discord.gg/papermc
++ Website: https://papermc.io/""";
++
++ private static final Function<ContextMap, String> WORLD_HEADER = map -> String.format("""
++ This is a world configuration file for Paper.
++ This file may start empty but can be filled with settings to override ones in the %s/%s
++
++ World: %s (%s)""",
++ PaperConfigurations.CONFIG_DIR,
++ PaperConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME,
++ map.require(WORLD_NAME),
++ map.require(WORLD_KEY)
++ );
++
++ private static final String MOVED_NOTICE = """
++ The global and world default configuration files have moved to %s
++ and the world-specific configuration file has been moved inside
++ the respective world folder.
++
++ See https://docs.papermc.io/paper/configuration for more information.
++ """;
++
++ @VisibleForTesting
++ public static final Supplier<SpigotWorldConfig> SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) {
++ @Override // override to ensure "verbose" is false
++ public void init() {
++ SpigotConfig.readConfig(SpigotWorldConfig.class, this);
++ }
++ });
++ public static final ContextKey<Supplier<SpigotWorldConfig>> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken<Supplier<SpigotWorldConfig>>() {}, "spigot world config");
++
++
++ public PaperConfigurations(final Path globalFolder) {
++ super(globalFolder, GlobalConfiguration.class, WorldConfiguration.class, GLOBAL_CONFIG_FILE_NAME, WORLD_DEFAULTS_CONFIG_FILE_NAME, WORLD_CONFIG_FILE_NAME);
++ }
++
++ @Override
++ protected int globalConfigVersion() {
++ return GlobalConfiguration.CURRENT_VERSION;
++ }
++
++ @Override
++ protected int worldConfigVersion() {
++ return WorldConfiguration.CURRENT_VERSION;
++ }
++
++ @Override
++ protected YamlConfigurationLoader.Builder createLoaderBuilder() {
++ return super.createLoaderBuilder()
++ .defaultOptions(PaperConfigurations::defaultOptions);
++ }
++
++ private static ConfigurationOptions defaultOptions(ConfigurationOptions options) {
++ return options.serializers(builder -> builder
++ .register(MapSerializer.TYPE, new MapSerializer(false))
++ .register(new EnumValueSerializer())
++ .register(new ComponentSerializer())
++ .register(IntOr.Default.SERIALIZER)
++ .register(IntOr.Disabled.SERIALIZER)
++ .register(DoubleOr.Default.SERIALIZER)
++ .register(DoubleOr.Disabled.SERIALIZER)
++ .register(BooleanOrDefault.SERIALIZER)
++ .register(Duration.SERIALIZER)
++ .register(DurationOrDisabled.SERIALIZER)
++ .register(NbtPathSerializer.SERIALIZER)
++ );
++ }
++
++ @Override
++ protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
++ return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder());
++ }
++
++ private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) {
++ return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig());
++ }
++
++ @Override
++ protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
++ return super.createGlobalLoaderBuilder()
++ .defaultOptions(PaperConfigurations::defaultGlobalOptions);
++ }
++
++ private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) {
++ return options
++ .header(GLOBAL_HEADER)
++ .serializers(builder -> builder
++ .register(new PacketClassSerializer())
++ );
++ }
++
++ @Override
++ public GlobalConfiguration initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
++ GlobalConfiguration configuration = super.initializeGlobalConfiguration(registryAccess);
++ GlobalConfiguration.set(configuration);
++ return configuration;
++ }
++
++ @Override
++ protected ContextMap.Builder createDefaultContextMap(final RegistryAccess registryAccess) {
++ return super.createDefaultContextMap(registryAccess)
++ .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, SPIGOT_WORLD_DEFAULTS);
++ }
++
++ @Override
++ protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) {
++ return super.createWorldObjectMapperFactoryBuilder(contextMap)
++ .addNodeResolver(new RequiresSpigotInitialization.Factory(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get()))
++ .addNodeResolver(new NestedSetting.Factory())
++ .addDiscoverer(InnerClassFieldDiscoverer.worldConfig(createWorldConfigInstance(contextMap)));
++ }
++
++ private static WorldConfiguration createWorldConfigInstance(ContextMap contextMap) {
++ return new WorldConfiguration(
++ contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(),
++ contextMap.require(Configurations.WORLD_KEY)
++ );
++ }
++
++ @Override
++ protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) {
++ final RegistryAccess access = contextMap.require(REGISTRY_ACCESS);
++ return super.createWorldConfigLoaderBuilder(contextMap)
++ .defaultOptions(options -> options
++ .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap))
++ .serializers(serializers -> serializers
++ .register(new TypeToken<Reference2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2IntMap<?>>(Reference2IntOpenHashMap::new, Integer.TYPE))
++ .register(new TypeToken<Reference2LongMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2LongMap<?>>(Reference2LongOpenHashMap::new, Long.TYPE))
++ .register(new TypeToken<Reference2ObjectMap<?, ?>>() {}, new FastutilMapSerializer.SomethingToSomething<Reference2ObjectMap<?, ?>>(Reference2ObjectOpenHashMap::new))
++ .register(new TypeToken<Table<?, ?, ?>>() {}, new TableSerializer())
++ .register(DespawnRange.class, DespawnRange.SERIALIZER)
++ .register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer())
++ .register(EngineMode.SERIALIZER)
++ .register(FallbackValueSerializer.create(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer))
++ .register(new RegistryValueSerializer<>(new TypeToken<EntityType<?>>() {}, access, Registries.ENTITY_TYPE, true))
++ .register(new RegistryValueSerializer<>(Item.class, access, Registries.ITEM, true))
++ .register(new RegistryValueSerializer<>(Block.class, access, Registries.BLOCK, true))
++ .register(new RegistryHolderSerializer<>(new TypeToken<ConfiguredFeature<?, ?>>() {}, access, Registries.CONFIGURED_FEATURE, false))
++ )
++ );
++ }
++
++ @Override
++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException {
++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
++ for (final NodePath path : RemovedConfigurations.REMOVED_WORLD_PATHS) {
++ builder.addAction(path, TransformAction.remove());
++ }
++ builder.build().apply(node);
++
++ final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
++ V29_ZeroWorldHeight.apply(versionedBuilder);
++ V30_RenameFilterNbtFromSpawnEgg.apply(versionedBuilder);
++ V31_SpawnLoadedRangeToGameRule.apply(versionedBuilder, contextMap, defaultsNode);
++ // ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
++ versionedBuilder.build().apply(node);
++ }
++
++ @Override
++ protected void applyGlobalConfigTransformations(ConfigurationNode node) throws ConfigurateException {
++ ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
++ for (NodePath path : RemovedConfigurations.REMOVED_GLOBAL_PATHS) {
++ builder.addAction(path, TransformAction.remove());
++ }
++ builder.build().apply(node);
++
++ final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
++ V29_LogIPs.apply(versionedBuilder);
++ // ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
++ versionedBuilder.build().apply(node);
++ }
++
++ private static final List<Transformations.DefaultsAware> DEFAULT_AWARE_TRANSFORMATIONS = List.of(
++ FeatureSeedsGeneration::apply
++ );
++
++ @Override
++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException {
++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
++ // ADD FUTURE TRANSFORMS HERE (these transforms run after the defaults have been merged into the node)
++ DEFAULT_AWARE_TRANSFORMATIONS.forEach(transform -> transform.apply(builder, contextMap, defaultsNode));
++ builder.build().apply(worldNode);
++ }
++
++ @Override
++ public WorldConfiguration createWorldConfig(final ContextMap contextMap) {
++ final String levelName = contextMap.require(WORLD_NAME);
++ try {
++ return super.createWorldConfig(contextMap);
++ } catch (IOException exception) {
++ throw new RuntimeException("Could not create world config for " + levelName, exception);
++ }
++ }
++
++ @Override
++ protected boolean isConfigType(final Type type) {
++ return ConfigurationPart.class.isAssignableFrom(erase(type));
++ }
++
++ public void reloadConfigs(MinecraftServer server) {
++ try {
++ this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get()));
++ this.initializeWorldDefaultsConfiguration(server.registryAccess());
++ for (ServerLevel level : server.getAllLevels()) {
++ this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
++ }
++ } catch (Exception ex) {
++ throw new RuntimeException("Could not reload paper configuration files", ex);
++ }
++ }
++
++ private static ContextMap createWorldContextMap(ServerLevel level) {
++ return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess(), level.getGameRules());
++ }
++
++ public static ContextMap createWorldContextMap(final Path dir, final String levelName, final ResourceLocation worldKey, final SpigotWorldConfig spigotConfig, final RegistryAccess registryAccess, final GameRules gameRules) {
++ return ContextMap.builder()
++ .put(WORLD_DIRECTORY, dir)
++ .put(WORLD_NAME, levelName)
++ .put(WORLD_KEY, worldKey)
++ .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, Suppliers.ofInstance(spigotConfig))
++ .put(REGISTRY_ACCESS, registryAccess)
++ .put(GAME_RULES, gameRules)
++ .build();
++ }
++
++ public static PaperConfigurations setup(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
++ final Path legacy = Files.isSymbolicLink(legacyConfig) ? Files.readSymbolicLink(legacyConfig) : legacyConfig;
++ if (needsConverting(legacyConfig)) {
++ final String legacyFileName = legacyConfig.getFileName().toString();
++ try {
++ if (Files.exists(configDir) && !Files.isDirectory(configDir)) {
++ throw new RuntimeException("Paper needs to create a '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + configDir.toAbsolutePath() + "'. Please remove it and restart the server.");
++ }
++ final Path backupDir = configDir.resolve(BACKUP_DIR);
++ if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) {
++ throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server.");
++ }
++ createDirectoriesSymlinkAware(backupDir);
++ final String backupFileName = legacyFileName + ".old";
++ final Path legacyConfigBackup = backupDir.resolve(backupFileName);
++ if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) {
++ throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + backupDir.toAbsolutePath() + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server.");
++ }
++ Files.move(legacyConfig.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup
++ if (Files.isSymbolicLink(legacyConfig)) {
++ Files.delete(legacyConfig);
++ }
++ final Path replacementFile = legacy.resolveSibling(legacyFileName + "-README.txt");
++ if (Files.notExists(replacementFile)) {
++ Files.createFile(replacementFile);
++ Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath()));
++ }
++ convert(legacyConfigBackup, configDir, worldFolder, spigotConfig);
++ } catch (final IOException ex) {
++ throw new RuntimeException("Could not convert '" + legacyFileName + "' to the new configuration format", ex);
++ }
++ }
++ try {
++ createDirectoriesSymlinkAware(configDir);
++ return new PaperConfigurations(configDir);
++ } catch (final IOException ex) {
++ throw new RuntimeException("Could not setup PaperConfigurations", ex);
++ }
++ }
++
++ private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
++ createDirectoriesSymlinkAware(configDir);
++
++ final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig);
++ final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
++ final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
++
++ final ConfigurationNode legacy = legacyLoader.load();
++ checkState(!legacy.virtual(), "can't be virtual");
++ final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt();
++
++ final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy();
++ checkState(!legacyWorldSettings.virtual(), "can't be virtual");
++ legacy.removeChild("world-settings");
++
++ // Apply legacy transformations before settings flatten
++ final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation
++ LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy);
++ spigotConfiguration.save(spigotConfig);
++ legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root
++ legacy.removeChild("settings");
++ LegacyPaperConfig.toNewFormat().apply(legacy);
++ globalLoader.save(legacy); // save converted node to new global location
++
++ final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy();
++ checkState(!worldDefaults.virtual());
++ worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
++ legacyWorldSettings.removeChild("default");
++ LegacyPaperWorldConfig.transformation().apply(worldDefaults);
++ LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults);
++ worldDefaultsLoader.save(worldDefaults);
++
++ legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> {
++ try {
++ legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
++ LegacyPaperWorldConfig.transformation().apply(legacyWorldNode);
++ LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode);
++ ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location
++ } catch (final ConfigurateException ex) {
++ ex.printStackTrace();
++ }
++ });
++ }
++
++ private static boolean needsConverting(final Path legacyConfig) {
++ return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig);
++ }
++
++ @Deprecated
++ public YamlConfiguration createLegacyObject(final MinecraftServer server) {
++ YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
++ ConfigurationSection worlds = global.createSection("__________WORLDS__________");
++ worlds.set("__defaults__", YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile()));
++ for (ServerLevel level : server.getAllLevels()) {
++ worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile()));
++ }
++ return global;
++ }
++
++ @Deprecated
++ public static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exception {
++ YamlConfiguration config = new YamlConfiguration();
++ if (configFile.exists()) {
++ try {
++ config.load(configFile);
++ } catch (Exception ex) {
++ throw new Exception("Failed to load configuration file: " + configFile.getName(), ex);
++ }
++ }
++ return config;
++ }
++
++ @VisibleForTesting
++ static ConfigurationNode createForTesting() {
++ ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build();
++ ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults()))
++ .serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer()));
++ return BasicConfigurationNode.root(options);
++ }
++
++ // Symlinks are not correctly checked in createDirectories
++ static void createDirectoriesSymlinkAware(Path path) throws IOException {
++ if (!Files.isDirectory(path)) {
++ Files.createDirectories(path);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f9a4bb664409a6c691c4dc901afe0bde75813636
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java
+@@ -0,0 +1,84 @@
++package io.papermc.paper.configuration;
++
++import org.spongepowered.configurate.NodePath;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++interface RemovedConfigurations {
++
++ NodePath[] REMOVED_WORLD_PATHS = {
++ path("falling-blocks-collide-with-signs"),
++ path("fast-drain"),
++ path("lava-flow-speed"),
++ path("load-chunks"),
++ path("misc", "boats-drop-boats"),
++ path("player-exhaustion"),
++ path("remove-unloaded"),
++ path("tick-next-tick-list-cap"),
++ path("tick-next-tick-list-cap-ignores-redstone"),
++ path("elytra-hit-wall-damage"),
++ path("queue-light-updates"),
++ path("save-queue-limit-for-auto-save"),
++ path("max-chunk-sends-per-tick"),
++ path("max-chunk-gens-per-tick"),
++ path("fire-physics-event-for-redstone"),
++ path("fix-zero-tick-instant-grow-farms"),
++ path("bed-search-radius"),
++ path("lightning-strike-distance-limit"),
++ path("fix-wither-targeting-bug"),
++ path("remove-corrupt-tile-entities"),
++ path("allow-undead-horse-leashing"),
++ path("reset-arrow-despawn-timer-on-fall"),
++ path("seed-based-feature-search"),
++ path("seed-based-feature-search-loads-chunks"),
++ path("viewdistances", "no-tick-view-distance"),
++ path("seed-based-feature-search"), // unneeded as of 1.18
++ path("seed-based-feature-search-loads-chunks"), // unneeded as of 1.18
++ path("reset-arrow-despawn-timer-on-fall"),
++ path("squid-spawn-height"),
++ path("viewdistances"),
++ path("use-alternate-fallingblock-onGround-detection"),
++ path("skip-entity-ticking-in-chunks-scheduled-for-unload"),
++ path("tracker-update-distance"),
++ path("allow-block-location-tab-completion"),
++ path("cache-chunk-maps"),
++ path("disable-mood-sounds"),
++ path("fix-cannons"),
++ path("player-blocking-damage-multiplier"),
++ path("remove-invalid-mob-spawner-tile-entities"),
++ path("use-hopper-check"),
++ path("use-async-lighting"),
++ path("tnt-explosion-volume"),
++ path("entities", "spawning", "despawn-ranges", "soft"),
++ path("entities", "spawning", "despawn-ranges", "hard"),
++ path("fixes", "fix-curing-zombie-villager-discount-exploit"),
++ path("entities", "mob-effects", "undead-immune-to-certain-effects"),
++ path("entities", "entities-target-with-follow-range")
++ };
++ // spawn.keep-spawn-loaded and spawn.keep-spawn-loaded-range are no longer used, but kept
++ // in the world default config for compatibility with old worlds being migrated to use the gamerule
++
++ NodePath[] REMOVED_GLOBAL_PATHS = {
++ path("data-value-allowed-items"),
++ path("effect-modifiers"),
++ path("stackable-buckets"),
++ path("async-chunks"),
++ path("queue-light-updates-max-loss"),
++ path("sleep-between-chunk-saves"),
++ path("remove-invalid-statistics"),
++ path("min-chunk-load-threads"),
++ path("use-versioned-world"),
++ path("save-player-data"), // to spigot (converted)
++ path("log-named-entity-deaths"), // default in vanilla
++ path("chunk-tasks-per-tick"), // removed in tuinity merge
++ path("item-validation", "loc-name"),
++ path("commandErrorMessage"),
++ path("baby-zombie-movement-speed"),
++ path("limit-player-interactions"),
++ path("warnWhenSettingExcessiveVelocity"),
++ path("logging", "use-rgb-for-named-text-colors"),
++ path("unsupported-settings", "allow-grindstone-overstacking"),
++ path("unsupported-settings", "allow-tripwire-disarming-exploits")
++ };
++
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b1c917d65076a3805e5b78cb946753f0c101e214
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
+@@ -0,0 +1,590 @@
++package io.papermc.paper.configuration;
++
++import com.google.common.collect.HashBasedTable;
++import com.google.common.collect.Table;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer;
++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization;
++import io.papermc.paper.configuration.mapping.MergeMap;
++import io.papermc.paper.configuration.serializer.NbtPathSerializer;
++import io.papermc.paper.configuration.serializer.collections.MapSerializer;
++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration;
++import io.papermc.paper.configuration.type.BooleanOrDefault;
++import io.papermc.paper.configuration.type.DespawnRange;
++import io.papermc.paper.configuration.type.Duration;
++import io.papermc.paper.configuration.type.DurationOrDisabled;
++import io.papermc.paper.configuration.type.EngineMode;
++import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate;
++import io.papermc.paper.configuration.type.fallback.AutosavePeriod;
++import io.papermc.paper.configuration.type.number.BelowZeroToEmpty;
++import io.papermc.paper.configuration.type.number.DoubleOr;
++import io.papermc.paper.configuration.type.number.IntOr;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2LongMap;
++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
++import java.util.Arrays;
++import java.util.IdentityHashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.OptionalDouble;
++import java.util.function.Function;
++import java.util.stream.Collectors;
++import net.minecraft.Util;
++import net.minecraft.commands.arguments.NbtPathArgument;
++import net.minecraft.core.Holder;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.world.Difficulty;
++import net.minecraft.world.entity.Display;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.entity.ExperienceOrb;
++import net.minecraft.world.entity.MobCategory;
++import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
++import net.minecraft.world.entity.decoration.HangingEntity;
++import net.minecraft.world.entity.item.ItemEntity;
++import net.minecraft.world.entity.monster.Vindicator;
++import net.minecraft.world.entity.monster.Zombie;
++import net.minecraft.world.entity.player.Player;
++import net.minecraft.world.item.Item;
++import net.minecraft.world.item.Items;
++import net.minecraft.world.level.NaturalSpawner;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
++import org.slf4j.Logger;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.meta.Comment;
++import org.spongepowered.configurate.objectmapping.meta.PostProcess;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++import org.spongepowered.configurate.objectmapping.meta.Setting;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
++public class WorldConfiguration extends ConfigurationPart {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ static final int CURRENT_VERSION = 31; // (when you change the version, change the comment, so it conflicts on rebases): migrate spawn loaded configs to gamerule
++
++ private final transient SpigotWorldConfig spigotConfig;
++ private final transient ResourceLocation worldKey;
++
++ WorldConfiguration(final SpigotWorldConfig spigotConfig, final ResourceLocation worldKey) {
++ this.spigotConfig = spigotConfig;
++ this.worldKey = worldKey;
++ }
++
++ public boolean isDefault() {
++ return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY);
++ }
++
++ @Setting(Configuration.VERSION_FIELD)
++ public int version = CURRENT_VERSION;
++
++ public Anticheat anticheat;
++
++ public class Anticheat extends ConfigurationPart {
++
++ public Obfuscation obfuscation;
++
++ public class Obfuscation extends ConfigurationPart {
++ public Items items = new Items();
++ public class Items extends ConfigurationPart {
++ public boolean hideItemmeta = false;
++ public boolean hideDurability = false;
++ public boolean hideItemmetaWithVisualEffects = false;
++ }
++ }
++
++ public AntiXray antiXray;
++
++ public class AntiXray extends ConfigurationPart {
++ public boolean enabled = false;
++ public EngineMode engineMode = EngineMode.HIDE;
++ public int maxBlockHeight = 64;
++ public int updateRadius = 2;
++ public boolean lavaObscures = false;
++ public boolean usePermission = false;
++ public List<Block> hiddenBlocks = List.of(
++ //<editor-fold desc="Anti-Xray Hidden Blocks" defaultstate="collapsed">
++ Blocks.COPPER_ORE,
++ Blocks.DEEPSLATE_COPPER_ORE,
++ Blocks.RAW_COPPER_BLOCK,
++ Blocks.GOLD_ORE,
++ Blocks.DEEPSLATE_GOLD_ORE,
++ Blocks.IRON_ORE,
++ Blocks.DEEPSLATE_IRON_ORE,
++ Blocks.RAW_IRON_BLOCK,
++ Blocks.COAL_ORE,
++ Blocks.DEEPSLATE_COAL_ORE,
++ Blocks.LAPIS_ORE,
++ Blocks.DEEPSLATE_LAPIS_ORE,
++ Blocks.MOSSY_COBBLESTONE,
++ Blocks.OBSIDIAN,
++ Blocks.CHEST,
++ Blocks.DIAMOND_ORE,
++ Blocks.DEEPSLATE_DIAMOND_ORE,
++ Blocks.REDSTONE_ORE,
++ Blocks.DEEPSLATE_REDSTONE_ORE,
++ Blocks.CLAY,
++ Blocks.EMERALD_ORE,
++ Blocks.DEEPSLATE_EMERALD_ORE,
++ Blocks.ENDER_CHEST
++ //</editor-fold>
++ );
++ public List<Block> replacementBlocks = List.of(Blocks.STONE, Blocks.OAK_PLANKS, Blocks.DEEPSLATE);
++ }
++ }
++
++ public Entities entities;
++
++ public class Entities extends ConfigurationPart {
++ public MobEffects mobEffects;
++
++ public class MobEffects extends ConfigurationPart {
++ public boolean spidersImmuneToPoisonEffect = true;
++ public ImmuneToWitherEffect immuneToWitherEffect;
++
++ public class ImmuneToWitherEffect extends ConfigurationPart {
++ public boolean wither = true;
++ public boolean witherSkeleton = true;
++ }
++ }
++
++ public ArmorStands armorStands;
++
++ public class ArmorStands extends ConfigurationPart {
++ public boolean doCollisionEntityLookups = true;
++ public boolean tick = true;
++ }
++
++ public Markers markers;
++
++ public class Markers extends ConfigurationPart {
++ public boolean tick = true;
++ }
++
++ public Sniffer sniffer;
++
++ public class Sniffer extends ConfigurationPart {
++ public IntOr.Default hatchTime = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default boostedHatchTime = IntOr.Default.USE_DEFAULT;
++ }
++
++ public Spawning spawning;
++
++ public class Spawning extends ConfigurationPart {
++ public ArrowDespawnRate nonPlayerArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
++ public ArrowDespawnRate creativeArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
++ public boolean filterBadTileEntityNbtFromFallingBlocks = true;
++ public List<NbtPathArgument.NbtPath> filteredEntityTagNbtPaths = NbtPathSerializer.fromString(List.of("Pos", "Motion", "SleepingX", "SleepingY", "SleepingZ"));
++ public boolean disableMobSpawnerSpawnEggTransformation = false;
++ public boolean perPlayerMobSpawns = true;
++ public boolean scanForLegacyEnderDragon = true;
++ @MergeMap
++ public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
++ @MergeMap
++ public Map<MobCategory, DespawnRangePair> despawnRanges = Arrays.stream(MobCategory.values()).collect(Collectors.toMap(Function.identity(), category -> DespawnRangePair.createDefault()));
++ public DespawnRange.Shape despawnRangeShape = DespawnRange.Shape.ELLIPSOID;
++ @MergeMap
++ public Reference2IntMap<MobCategory> ticksPerSpawn = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
++
++ @ConfigSerializable
++ public record DespawnRangePair(@Required DespawnRange hard, @Required DespawnRange soft) {
++ public static DespawnRangePair createDefault() {
++ return new DespawnRangePair(
++ new DespawnRange(IntOr.Default.USE_DEFAULT),
++ new DespawnRange(IntOr.Default.USE_DEFAULT)
++ );
++ }
++ }
++
++ @MapSerializer.ThrowExceptions
++ public Reference2ObjectMap<EntityType<?>, IntOr.Disabled> despawnTime = Util.make(new Reference2ObjectOpenHashMap<>(), map -> {
++ map.put(EntityType.SNOWBALL, IntOr.Disabled.DISABLED);
++ map.put(EntityType.LLAMA_SPIT, IntOr.Disabled.DISABLED);
++ });
++
++ @PostProcess
++ public void precomputeDespawnDistances() throws SerializationException {
++ for (Map.Entry<MobCategory, DespawnRangePair> entry : this.despawnRanges.entrySet()) {
++ final MobCategory category = entry.getKey();
++ final DespawnRangePair range = entry.getValue();
++ range.hard().preComputed(category.getDespawnDistance(), category.getSerializedName());
++ range.soft().preComputed(category.getNoDespawnDistance(), category.getSerializedName());
++ }
++ }
++
++ public WaterAnimalSpawnHeight wateranimalSpawnHeight;
++
++ public class WaterAnimalSpawnHeight extends ConfigurationPart {
++ public IntOr.Default maximum = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default minimum = IntOr.Default.USE_DEFAULT;
++ }
++
++ public SlimeSpawnHeight slimeSpawnHeight;
++
++ public class SlimeSpawnHeight extends ConfigurationPart {
++
++ public SurfaceSpawnableSlimeBiome surfaceBiome;
++
++ public class SurfaceSpawnableSlimeBiome extends ConfigurationPart {
++ public double maximum = 70;
++ public double minimum = 50;
++ }
++
++ public SlimeChunk slimeChunk;
++
++ public class SlimeChunk extends ConfigurationPart {
++ public double maximum = 40;
++ }
++ }
++
++ public WanderingTrader wanderingTrader;
++
++ public class WanderingTrader extends ConfigurationPart {
++ public int spawnMinuteLength = 1200;
++ public int spawnDayLength = 24000;
++ public int spawnChanceFailureIncrement = 25;
++ public int spawnChanceMin = 25;
++ public int spawnChanceMax = 75;
++ }
++
++ public boolean allChunksAreSlimeChunks = false;
++ @BelowZeroToEmpty
++ public DoubleOr.Default skeletonHorseThunderSpawnChance = DoubleOr.Default.USE_DEFAULT;
++ public boolean ironGolemsCanSpawnInAir = false;
++ public boolean countAllMobsForSpawning = false;
++ @BelowZeroToEmpty
++ public IntOr.Default monsterSpawnMaxLightLevel = IntOr.Default.USE_DEFAULT;
++ public DuplicateUUID duplicateUuid;
++
++ public class DuplicateUUID extends ConfigurationPart {
++ public DuplicateUUIDMode mode = DuplicateUUIDMode.SAFE_REGEN;
++ public int safeRegenDeleteRange = 32;
++
++ public enum DuplicateUUIDMode {
++ SAFE_REGEN, DELETE, NOTHING, WARN;
++ }
++ }
++ public AltItemDespawnRate altItemDespawnRate;
++
++ public class AltItemDespawnRate extends ConfigurationPart {
++ public boolean enabled = false;
++ public Reference2IntMap<Item> items = new Reference2IntOpenHashMap<>(Map.of(Items.COBBLESTONE, 300));
++ }
++ }
++
++ public Behavior behavior;
++
++ public class Behavior extends ConfigurationPart {
++ public boolean disableChestCatDetection = false;
++ public boolean spawnerNerfedMobsShouldJump = false;
++ public int experienceMergeMaxValue = -1;
++ public boolean shouldRemoveDragon = false;
++ public boolean zombiesTargetTurtleEggs = true;
++ public boolean piglinsGuardChests = true;
++ public double babyZombieMovementModifier = 0.5;
++ public boolean allowSpiderWorldBorderClimbing = true;
++
++ private static final List<EntityType<?>> ZOMBIE_LIKE = List.of(EntityType.ZOMBIE, EntityType.HUSK, EntityType.ZOMBIE_VILLAGER, EntityType.ZOMBIFIED_PIGLIN);
++ @MergeMap
++ public Map<EntityType<?>, List<Difficulty>> doorBreakingDifficulty = Util.make(new IdentityHashMap<>(), map -> {
++ for (final EntityType<?> type : ZOMBIE_LIKE) {
++ map.put(type, Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList());
++ }
++ map.put(EntityType.VINDICATOR, Arrays.stream(Difficulty.values()).filter(Vindicator.DOOR_BREAKING_PREDICATE).toList());
++ });
++
++ public boolean disableCreeperLingeringEffect = false;
++ public boolean enderDragonsDeathAlwaysPlacesDragonEgg = false;
++ public boolean phantomsDoNotSpawnOnCreativePlayers = true;
++ public boolean phantomsOnlyAttackInsomniacs = true;
++ public int playerInsomniaStartTicks = 72000;
++ public int phantomsSpawnAttemptMinSeconds = 60;
++ public int phantomsSpawnAttemptMaxSeconds = 119;
++ public boolean parrotsAreUnaffectedByPlayerMovement = false;
++ @BelowZeroToEmpty
++ public DoubleOr.Default zombieVillagerInfectionChance = DoubleOr.Default.USE_DEFAULT;
++ public MobsCanAlwaysPickUpLoot mobsCanAlwaysPickUpLoot;
++
++ public class MobsCanAlwaysPickUpLoot extends ConfigurationPart {
++ public boolean zombies = false;
++ public boolean skeletons = false;
++ }
++
++ public boolean disablePlayerCrits = false;
++ public boolean nerfPigmenFromNetherPortals = false;
++ @Comment("Prevents merging items that are not on the same y level, preventing potential visual artifacts.")
++ public boolean onlyMergeItemsHorizontally = false;
++ public PillagerPatrols pillagerPatrols;
++
++ public class PillagerPatrols extends ConfigurationPart {
++ public boolean disable = false;
++ public double spawnChance = 0.2;
++ public SpawnDelay spawnDelay;
++ public Start start;
++
++ public class SpawnDelay extends ConfigurationPart {
++ public boolean perPlayer = false;
++ public int ticks = 12000;
++ }
++
++ public class Start extends ConfigurationPart {
++ public boolean perPlayer = false;
++ public int day = 5;
++ }
++ }
++ }
++
++ public TrackingRangeY trackingRangeY;
++
++ public class TrackingRangeY extends ConfigurationPart {
++ public boolean enabled = false;
++ public IntOr.Default player = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default animal = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default monster = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default misc = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default display = IntOr.Default.USE_DEFAULT;
++ public IntOr.Default other = IntOr.Default.USE_DEFAULT;
++
++ public int get(Entity entity, int def) {
++ if (entity instanceof EnderDragon) {
++ return -1; // Ender dragon is exempt
++ } else if (entity instanceof Display) {
++ return display.or(def);
++ } else if (entity instanceof Player) {
++ return player.or(def);
++ } else if (entity instanceof HangingEntity || entity instanceof ItemEntity || entity instanceof ExperienceOrb) {
++ return misc.or(def);
++ }
++ switch (entity.activationType) {
++ case ANIMAL, WATER, VILLAGER -> {
++ return animal.or(def);
++ }
++ case MONSTER, FLYING_MONSTER, RAIDER -> {
++ return monster.or(def);
++ }
++ default -> {
++ return other.or(def);
++ }
++ }
++ }
++ }
++ }
++
++ public Lootables lootables;
++
++ public class Lootables extends ConfigurationPart {
++ public boolean autoReplenish = false;
++ public boolean restrictPlayerReloot = true;
++ public DurationOrDisabled restrictPlayerRelootTime = DurationOrDisabled.USE_DISABLED;
++ public boolean resetSeedOnFill = true;
++ public int maxRefills = -1;
++ public Duration refreshMin = Duration.of("12h");
++ public Duration refreshMax = Duration.of("2d");
++ public boolean retainUnlootedShulkerBoxLootTableOnNonPlayerBreak = true;
++ }
++
++ public MaxGrowthHeight maxGrowthHeight;
++
++ public class MaxGrowthHeight extends ConfigurationPart {
++ public int cactus = 3;
++ public int reeds = 3;
++ public Bamboo bamboo;
++
++ public class Bamboo extends ConfigurationPart {
++ public int max = 16;
++ public int min = 11;
++ }
++ }
++
++ public Scoreboards scoreboards;
++
++ public class Scoreboards extends ConfigurationPart {
++ public boolean allowNonPlayerEntitiesOnScoreboards = true;
++ public boolean useVanillaWorldScoreboardNameColoring = false;
++ }
++
++ public Environment environment;
++
++ public class Environment extends ConfigurationPart {
++ public boolean disableThunder = false;
++ public boolean disableIceAndSnow = false;
++ public boolean optimizeExplosions = false;
++ public boolean disableExplosionKnockback = false;
++ public boolean generateFlatBedrock = false;
++ public FrostedIce frostedIce;
++ public DoubleOr.Disabled voidDamageAmount = new DoubleOr.Disabled(OptionalDouble.of(4));
++ public double voidDamageMinBuildHeightOffset = -64.0;
++
++ public class FrostedIce extends ConfigurationPart {
++ public boolean enabled = true;
++ public Delay delay;
++
++ public class Delay extends ConfigurationPart {
++ public int min = 20;
++ public int max = 40;
++ }
++ }
++
++ public TreasureMaps treasureMaps;
++ public class TreasureMaps extends ConfigurationPart {
++ public boolean enabled = true;
++ @NestedSetting({"find-already-discovered", "villager-trade"})
++ public boolean findAlreadyDiscoveredVillager = false;
++ @NestedSetting({"find-already-discovered", "loot-tables"})
++ public BooleanOrDefault findAlreadyDiscoveredLootTable = BooleanOrDefault.USE_DEFAULT;
++ }
++
++ public int fireTickDelay = 30;
++ public int waterOverLavaFlowSpeed = 5;
++ public int portalSearchRadius = 128;
++ public int portalCreateRadius = 16;
++ public boolean portalSearchVanillaDimensionScaling = true;
++ public boolean disableTeleportationSuffocationCheck = false;
++ public IntOr.Disabled netherCeilingVoidDamageHeight = IntOr.Disabled.DISABLED;
++ public int maxFluidTicks = 65536;
++ public int maxBlockTicks = 65536;
++ public boolean locateStructuresOutsideWorldBorder = false;
++ }
++
++ public Spawn spawn;
++
++ public class Spawn extends ConfigurationPart {
++ public boolean allowUsingSignsInsideSpawnProtection = false;
++ }
++
++ public Maps maps;
++
++ public class Maps extends ConfigurationPart {
++ public int itemFrameCursorLimit = 128;
++ public int itemFrameCursorUpdateInterval = 10;
++ }
++
++ public Fixes fixes;
++
++ public class Fixes extends ConfigurationPart {
++ public boolean fixItemsMergingThroughWalls = false;
++ public boolean disableUnloadedChunkEnderpearlExploit = false;
++ public boolean preventTntFromMovingInWater = false;
++ public boolean splitOverstackedLoot = true;
++ public IntOr.Disabled fallingBlockHeightNerf = IntOr.Disabled.DISABLED;
++ public IntOr.Disabled tntEntityHeightNerf = IntOr.Disabled.DISABLED;
++ }
++
++ public UnsupportedSettings unsupportedSettings;
++
++ public class UnsupportedSettings extends ConfigurationPart {
++ public boolean fixInvulnerableEndCrystalExploit = true;
++ public boolean disableWorldTickingWhenEmpty = false;
++ }
++
++ public Hopper hopper;
++
++ public class Hopper extends ConfigurationPart {
++ public boolean cooldownWhenFull = true;
++ public boolean disableMoveEvent = false;
++ public boolean ignoreOccludingBlocks = false;
++ }
++
++ public Collisions collisions;
++
++ public class Collisions extends ConfigurationPart {
++ public boolean onlyPlayersCollide = false;
++ public boolean allowVehicleCollisions = true;
++ public boolean fixClimbingBypassingCrammingRule = false;
++ @RequiresSpigotInitialization(MaxEntityCollisionsInitializer.class)
++ public int maxEntityCollisions = 8;
++ public boolean allowPlayerCrammingDamage = false;
++ }
++
++ public Chunks chunks;
++
++ public class Chunks extends ConfigurationPart {
++ public AutosavePeriod autoSaveInterval = AutosavePeriod.def();
++ public int maxAutoSaveChunksPerTick = 24;
++ public int fixedChunkInhabitedTime = -1;
++ public boolean preventMovingIntoUnloadedChunks = false;
++ public Duration delayChunkUnloadsBy = Duration.of("10s");
++ public Reference2IntMap<EntityType<?>> entityPerChunkSaveLimit = Util.make(new Reference2IntOpenHashMap<>(BuiltInRegistries.ENTITY_TYPE.size()), map -> {
++ map.defaultReturnValue(-1);
++ map.put(EntityType.EXPERIENCE_ORB, -1);
++ map.put(EntityType.SNOWBALL, -1);
++ map.put(EntityType.ENDER_PEARL, -1);
++ map.put(EntityType.ARROW, -1);
++ map.put(EntityType.FIREBALL, -1);
++ map.put(EntityType.SMALL_FIREBALL, -1);
++ });
++ public boolean flushRegionsOnSave = false;
++ }
++
++ public FishingTimeRange fishingTimeRange;
++
++ public class FishingTimeRange extends ConfigurationPart {
++ public int minimum = 100;
++ public int maximum = 600;
++ }
++
++ public TickRates tickRates;
++
++ public class TickRates extends ConfigurationPart {
++ public int grassSpread = 1;
++ public int containerUpdate = 1;
++ public int mobSpawner = 1;
++ public int wetFarmland = 1;
++ public int dryFarmland = 1;
++ public Table<EntityType<?>, String, Integer> sensor = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "secondarypoisensor", 40));
++ public Table<EntityType<?>, String, Integer> behavior = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "validatenearbypoi", -1));
++ }
++
++ @Setting(FeatureSeedsGeneration.FEATURE_SEEDS_KEY)
++ public FeatureSeeds featureSeeds;
++
++ public class FeatureSeeds extends ConfigurationPart {
++ @SuppressWarnings("unused") // Is used in FeatureSeedsGeneration
++ @Setting(FeatureSeedsGeneration.GENERATE_KEY)
++ public boolean generateRandomSeedsForAll = false;
++ @Setting(FeatureSeedsGeneration.FEATURES_KEY)
++ public Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = new Reference2LongOpenHashMap<>();
++
++ @PostProcess
++ private void postProcess() {
++ this.features.defaultReturnValue(-1);
++ }
++ }
++
++ public CommandBlocks commandBlocks;
++
++ public class CommandBlocks extends ConfigurationPart {
++ public int permissionsLevel = 2;
++ public boolean forceFollowPermLevel = true;
++ }
++
++ public Misc misc;
++
++ public class Misc extends ConfigurationPart {
++ public int lightQueueSize = 20;
++ public boolean updatePathfindingOnBlockUpdate = true;
++ public boolean showSignClickCommandFailureMsgsToPlayer = false;
++ public RedstoneImplementation redstoneImplementation = RedstoneImplementation.VANILLA;
++ public AlternateCurrentUpdateOrder alternateCurrentUpdateOrder = AlternateCurrentUpdateOrder.HORIZONTAL_FIRST_OUTWARD;
++ public boolean disableEndCredits = false;
++ public DoubleOr.Default maxLeashDistance = DoubleOr.Default.USE_DEFAULT;
++ public boolean disableSprintInterruptionOnAttack = false;
++ public int shieldBlockingDelay = 5;
++ public boolean disableRelativeProjectileVelocity = false;
++ public boolean legacyEnderPearlBehavior = false;
++
++ public enum RedstoneImplementation {
++ VANILLA, EIGENCRAFT, ALTERNATE_CURRENT
++ }
++
++ public enum AlternateCurrentUpdateOrder {
++ HORIZONTAL_FIRST_OUTWARD, HORIZONTAL_FIRST_INWARD, VERTICAL_FIRST_OUTWARD, VERTICAL_FIRST_INWARD
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..514be9a11e2ca368ea72dd2bac1b84bff5468814
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java
+@@ -0,0 +1,30 @@
++package io.papermc.paper.configuration.constraint;
++
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.Constructor;
++import java.lang.reflect.Type;
++
++@Documented
++@Retention(RetentionPolicy.RUNTIME)
++@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
++public @interface Constraint {
++ Class<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> value();
++
++ class Factory implements org.spongepowered.configurate.objectmapping.meta.Constraint.Factory<Constraint, Object> {
++ @SuppressWarnings("unchecked")
++ @Override
++ public org.spongepowered.configurate.objectmapping.meta.Constraint<Object> make(final Constraint data, final Type type) {
++ try {
++ final Constructor<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> constructor = data.value().getDeclaredConstructor();
++ constructor.trySetAccessible();
++ return (org.spongepowered.configurate.objectmapping.meta.Constraint<Object>) constructor.newInstance();
++ } catch (final ReflectiveOperationException e) {
++ throw new RuntimeException("Could not create constraint", e);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9cab83a5b47b29d394bdf6e5c5f8e2c9952d9156
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java
+@@ -0,0 +1,43 @@
++package io.papermc.paper.configuration.constraint;
++
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.Type;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.objectmapping.meta.Constraint;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public final class Constraints {
++ private Constraints() {
++ }
++
++ public static final class Positive implements Constraint<Number> {
++ @Override
++ public void validate(@Nullable Number value) throws SerializationException {
++ if (value != null && value.doubleValue() <= 0) {
++ throw new SerializationException(value + " should be positive");
++ }
++ }
++ }
++
++ @Documented
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target(ElementType.FIELD)
++ public @interface Min {
++ int value();
++
++ final class Factory implements Constraint.Factory<Min, Number> {
++ @Override
++ public Constraint<Number> make(Min data, Type type) {
++ return value -> {
++ if (value != null && value.intValue() < data.value()) {
++ throw new SerializationException(value + " is less than the min " + data.value());
++ }
++ };
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ddef1ed3ff6fef52a70ee8bbf0b7607f6588ae6d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java
+@@ -0,0 +1,29 @@
++package io.papermc.paper.configuration.legacy;
++
++import org.jspecify.annotations.Nullable;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++import org.spongepowered.configurate.util.NamingSchemes;
++
++public class MaxEntityCollisionsInitializer implements NodeResolver {
++
++ private final String name;
++ private final SpigotWorldConfig spigotConfig;
++
++ public MaxEntityCollisionsInitializer(String name, SpigotWorldConfig spigotConfig) {
++ this.name = name;
++ this.spigotConfig = spigotConfig;
++ }
++
++ @Override
++ public @Nullable ConfigurationNode resolve(ConfigurationNode parent) {
++ final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name);
++ final ConfigurationNode node = parent.node(key);
++ final int old = this.spigotConfig.getInt("max-entity-collisions", -1, false);
++ if (node.virtual() && old > -1) {
++ node.raw(old);
++ }
++ return node;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..81c64a2ffad4bcd69f0012f04567a7d15f2d6dd5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java
+@@ -0,0 +1,48 @@
++package io.papermc.paper.configuration.legacy;
++
++import com.google.common.collect.HashBasedTable;
++import com.google.common.collect.Table;
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.AnnotatedElement;
++import java.lang.reflect.Constructor;
++import org.jspecify.annotations.Nullable;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++
++@Documented
++@Retention(RetentionPolicy.RUNTIME)
++@Target(ElementType.FIELD)
++public @interface RequiresSpigotInitialization {
++
++ Class<? extends NodeResolver> value();
++
++ final class Factory implements NodeResolver.Factory {
++
++ private final SpigotWorldConfig spigotWorldConfig;
++ private final Table<Class<? extends NodeResolver>, String, NodeResolver> cache = HashBasedTable.create();
++
++ public Factory(SpigotWorldConfig spigotWorldConfig) {
++ this.spigotWorldConfig = spigotWorldConfig;
++ }
++
++ @Override
++ public @Nullable NodeResolver make(String name, AnnotatedElement element) {
++ if (element.isAnnotationPresent(RequiresSpigotInitialization.class)) {
++ return this.cache.row(element.getAnnotation(RequiresSpigotInitialization.class).value()).computeIfAbsent(name, key -> {
++ try {
++ final Constructor<? extends NodeResolver> constructor = element.getAnnotation(RequiresSpigotInitialization.class).value().getDeclaredConstructor(String.class, SpigotWorldConfig.class);
++ constructor.trySetAccessible();
++ return constructor.newInstance(key, this.spigotWorldConfig);
++ } catch (final ReflectiveOperationException e) {
++ throw new RuntimeException("Could not create constraint", e);
++ }
++ });
++ }
++ return null;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fe5cc1c097f8d8c135e6ead6f458426bb84a8ebe
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java
+@@ -0,0 +1,27 @@
++package io.papermc.paper.configuration.legacy;
++
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++import org.spongepowered.configurate.util.NamingSchemes;
++
++public final class SpawnLoadedRangeInitializer implements NodeResolver {
++
++ private final String name;
++ private final SpigotWorldConfig spigotConfig;
++
++ public SpawnLoadedRangeInitializer(String name, SpigotWorldConfig spigotConfig) {
++ this.name = name;
++ this.spigotConfig = spigotConfig;
++ }
++
++ @Override
++ public ConfigurationNode resolve(ConfigurationNode parent) {
++ final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name);
++ final ConfigurationNode node = parent.node(key);
++ if (node.virtual()) {
++ node.raw(Math.min(spigotConfig.viewDistance, 10));
++ }
++ return node;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..05339a176083af667c16f77d76dc1878dafce3f0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java
+@@ -0,0 +1,52 @@
++package io.papermc.paper.configuration.mapping;
++
++import io.papermc.paper.configuration.ConfigurationPart;
++import io.papermc.paper.configuration.WorldConfiguration;
++import java.lang.reflect.AnnotatedType;
++import java.lang.reflect.Field;
++import java.util.Collections;
++import java.util.Map;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.objectmapping.FieldDiscoverer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
++
++ private final InnerClassInstanceSupplier instanceSupplier;
++ private final FieldDiscoverer<Map<Field, Object>> delegate;
++
++ @SuppressWarnings("unchecked")
++ public InnerClassFieldDiscoverer(final Map<Class<?>, Object> initialOverrides) {
++ this.instanceSupplier = new InnerClassInstanceSupplier(initialOverrides);
++ this.delegate = (FieldDiscoverer<Map<Field, Object>>) FieldDiscoverer.object(this.instanceSupplier);
++ }
++
++ @Override
++ public @Nullable <V> InstanceFactory<Map<Field, Object>> discover(final AnnotatedType target, final FieldCollector<Map<Field, Object>, V> collector) throws SerializationException {
++ final Class<?> clazz = erase(target.getType());
++ if (ConfigurationPart.class.isAssignableFrom(clazz)) {
++ final FieldDiscoverer.@Nullable InstanceFactory<Map<Field, Object>> instanceFactoryDelegate = this.delegate.<V>discover(target, (name, type, annotations, deserializer, serializer) -> {
++ if (!erase(type.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes
++ collector.accept(name, type, annotations, deserializer, serializer);
++ }
++ });
++ if (instanceFactoryDelegate instanceof MutableInstanceFactory<Map<Field, Object>> mutableInstanceFactoryDelegate) {
++ return new InnerClassInstanceFactory(this.instanceSupplier, mutableInstanceFactoryDelegate, target);
++ }
++ }
++ return null;
++ }
++
++ public static FieldDiscoverer<?> worldConfig(WorldConfiguration worldConfiguration) {
++ final Map<Class<?>, Object> overrides = Map.of(
++ WorldConfiguration.class, worldConfiguration
++ );
++ return new InnerClassFieldDiscoverer(overrides);
++ }
++
++ public static FieldDiscoverer<?> globalConfig() {
++ return new InnerClassFieldDiscoverer(Collections.emptyMap());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..25e3152c3307175da734a1cad7f7a4166e233021
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java
+@@ -0,0 +1,64 @@
++package io.papermc.paper.configuration.mapping;
++
++import java.lang.reflect.AnnotatedType;
++import java.lang.reflect.Field;
++import java.util.Iterator;
++import java.util.Map;
++import java.util.Objects;
++import org.spongepowered.configurate.objectmapping.FieldDiscoverer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++final class InnerClassInstanceFactory implements FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> {
++
++ private final InnerClassInstanceSupplier instanceSupplier;
++ private final FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> fallback;
++ private final AnnotatedType targetType;
++
++ InnerClassInstanceFactory(final InnerClassInstanceSupplier instanceSupplier, final FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> fallback, final AnnotatedType targetType) {
++ this.instanceSupplier = instanceSupplier;
++ this.fallback = fallback;
++ this.targetType = targetType;
++ }
++
++ @Override
++ public Map<Field, Object> begin() {
++ return this.fallback.begin();
++ }
++
++ @SuppressWarnings("unchecked")
++ @Override
++ public void complete(final Object instance, final Map<Field, Object> intermediate) throws SerializationException {
++ final Iterator<Map.Entry<Field, Object>> iter = intermediate.entrySet().iterator();
++ try {
++ while (iter.hasNext()) { // manually merge any mergeable maps
++ Map.Entry<Field, Object> entry = iter.next();
++ if (entry.getKey().isAnnotationPresent(MergeMap.class) && Map.class.isAssignableFrom(entry.getKey().getType()) && intermediate.get(entry.getKey()) instanceof Map<?, ?> map) {
++ iter.remove();
++ Map<Object, Object> existingMap = (Map<Object, Object>) entry.getKey().get(instance);
++ if (existingMap != null) {
++ existingMap.putAll(map);
++ } else {
++ entry.getKey().set(instance, entry.getValue());
++ }
++ }
++ }
++ } catch (final IllegalAccessException e) {
++ throw new SerializationException(this.targetType.getType(), e);
++ }
++ this.fallback.complete(instance, intermediate);
++ }
++
++ @Override
++ public Object complete(final Map<Field, Object> intermediate) throws SerializationException {
++ final Object targetInstance = Objects.requireNonNull(this.instanceSupplier.instanceMap().get(erase(this.targetType.getType())), () -> this.targetType.getType() + " must already have an instance created");
++ this.complete(targetInstance, intermediate);
++ return targetInstance;
++ }
++
++ @Override
++ public boolean canCreateInstances() {
++ return this.fallback.canCreateInstances();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3778c47f563fd0011659234fc8394e3d59325782
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java
+@@ -0,0 +1,72 @@
++package io.papermc.paper.configuration.mapping;
++
++import io.papermc.paper.configuration.ConfigurationPart;
++import java.lang.reflect.AnnotatedType;
++import java.lang.reflect.Constructor;
++import java.lang.reflect.Modifier;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.function.Supplier;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.util.CheckedFunction;
++import org.spongepowered.configurate.util.CheckedSupplier;
++
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++/**
++ * This instance factory handles creating non-static inner classes by tracking all instances of objects that extend
++ * {@link ConfigurationPart}. Only 1 instance of each {@link ConfigurationPart} should be present for each instance
++ * of the field discoverer this is used in.
++ */
++final class InnerClassInstanceSupplier implements CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> {
++
++ private final Map<Class<?>, Object> instanceMap = new HashMap<>();
++ private final Map<Class<?>, Object> initialOverrides;
++
++ /**
++ * @param initialOverrides map of types to objects to preload the config objects with.
++ */
++ InnerClassInstanceSupplier(final Map<Class<?>, Object> initialOverrides) {
++ this.initialOverrides = initialOverrides;
++ }
++
++ @Override
++ public Supplier<Object> apply(final AnnotatedType target) throws SerializationException {
++ final Class<?> type = erase(target.getType());
++ if (this.initialOverrides.containsKey(type)) {
++ this.instanceMap.put(type, this.initialOverrides.get(type));
++ return () -> this.initialOverrides.get(type);
++ }
++ if (ConfigurationPart.class.isAssignableFrom(type) && !this.instanceMap.containsKey(type)) {
++ try {
++ final Constructor<?> constructor;
++ final CheckedSupplier<Object, ReflectiveOperationException> instanceSupplier;
++ if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) {
++ final Object instance = this.instanceMap.get(type.getEnclosingClass());
++ if (instance == null) {
++ throw new SerializationException("Cannot create a new instance of an inner class " + type.getName() + " without an instance of its enclosing class " + type.getEnclosingClass().getName());
++ }
++ constructor = type.getDeclaredConstructor(type.getEnclosingClass());
++ instanceSupplier = () -> constructor.newInstance(instance);
++ } else {
++ constructor = type.getDeclaredConstructor();
++ instanceSupplier = constructor::newInstance;
++ }
++ constructor.setAccessible(true);
++ final Object instance = instanceSupplier.get();
++ this.instanceMap.put(type, instance);
++ return () -> instance;
++ } catch (ReflectiveOperationException e) {
++ throw new SerializationException(ConfigurationPart.class, target + " must be a valid ConfigurationPart", e);
++ }
++ } else {
++ throw new SerializationException(target + " must be a valid ConfigurationPart");
++ }
++ }
++
++ Map<Class<?>, Object> instanceMap() {
++ return this.instanceMap;
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..471b161ac51900672434c6608595bb73c02d8180
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.configuration.mapping;
++
++import io.papermc.paper.configuration.ConfigurationPart;
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++
++/**
++ * For use in maps inside {@link ConfigurationPart}s that have default keys that shouldn't be removed by users
++ * <p>
++ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate
++ * keys overtime.
++ */
++@Documented
++@Target(ElementType.FIELD)
++@Retention(RetentionPolicy.RUNTIME)
++public @interface MergeMap {
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/mapping/package-info.java b/src/main/java/io/papermc/paper/configuration/mapping/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..454e6ec7ebf9c54a38d6ba3a73e7c197c67c2c00
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/mapping/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.mapping;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/package-info.java b/src/main/java/io/papermc/paper/configuration/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..11bf7bb357305cf90c201444be08dc71c14b7505
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9c339ef178ebc3b0251095f320e4a7a3656d3521
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java
+@@ -0,0 +1,26 @@
++package io.papermc.paper.configuration.serializer;
++
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.minimessage.MiniMessage;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.lang.reflect.Type;
++import java.util.function.Predicate;
++
++public class ComponentSerializer extends ScalarSerializer<Component> {
++
++ public ComponentSerializer() {
++ super(Component.class);
++ }
++
++ @Override
++ public Component deserialize(Type type, Object obj) throws SerializationException {
++ return MiniMessage.miniMessage().deserialize(obj.toString());
++ }
++
++ @Override
++ protected Object serialize(Component component, Predicate<Class<?>> typeSupported) {
++ return MiniMessage.miniMessage().serialize(component);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..27c0679d376bb31ab52131dfea74b3b580ca92b5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java
+@@ -0,0 +1,33 @@
++package io.papermc.paper.configuration.serializer;
++
++import io.papermc.paper.configuration.type.EngineMode;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.lang.reflect.Type;
++import java.util.function.Predicate;
++
++public final class EngineModeSerializer extends ScalarSerializer<EngineMode> {
++
++ public EngineModeSerializer() {
++ super(EngineMode.class);
++ }
++
++ @Override
++ public EngineMode deserialize(Type type, Object obj) throws SerializationException {
++ if (obj instanceof Integer id) {
++ try {
++ return EngineMode.valueOf(id);
++ } catch (IllegalArgumentException e) {
++ throw new SerializationException(id + " is not a valid id for type " + type + " for this node");
++ }
++ }
++
++ throw new SerializationException(obj + " is not of a valid type " + type + " for this node");
++ }
++
++ @Override
++ protected Object serialize(EngineMode item, Predicate<Class<?>> typeSupported) {
++ return item.getId();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d24d1480e3ee7e5004c2dcbe826823aa427f787a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java
+@@ -0,0 +1,49 @@
++package io.papermc.paper.configuration.serializer;
++
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import java.lang.reflect.Type;
++import java.util.Arrays;
++import java.util.List;
++import java.util.function.Predicate;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.util.EnumLookup;
++
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++/**
++ * Enum serializer that lists options if fails and accepts `-` as `_`.
++ */
++public class EnumValueSerializer extends ScalarSerializer<Enum<?>> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ public EnumValueSerializer() {
++ super(new TypeToken<Enum<?>>() {});
++ }
++
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ @Override
++ public @Nullable Enum<?> deserialize(final Type type, final Object obj) throws SerializationException {
++ final String enumConstant = obj.toString();
++ final Class<? extends Enum> typeClass = erase(type).asSubclass(Enum.class);
++ Enum<?> ret = EnumLookup.lookupEnum(typeClass, enumConstant);
++ if (ret == null) {
++ ret = EnumLookup.lookupEnum(typeClass, enumConstant.replace("-", "_"));
++ }
++ if (ret == null) {
++ boolean longer = typeClass.getEnumConstants().length > 10;
++ List<String> options = Arrays.stream(typeClass.getEnumConstants()).limit(10L).map(Enum::name).toList();
++ LOGGER.error("Invalid enum constant provided, expected one of [{}{}], but got {}", String.join(", ", options), longer ? ", ..." : "", enumConstant);
++ }
++ return ret;
++ }
++
++ @Override
++ public Object serialize(final Enum<?> item, final Predicate<Class<?>> typeSupported) {
++ return item.name();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b44b2dc28f619594e302417848e95c0087acbcea
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/NbtPathSerializer.java
+@@ -0,0 +1,52 @@
++package io.papermc.paper.configuration.serializer;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import com.mojang.brigadier.StringReader;
++import com.mojang.brigadier.exceptions.CommandSyntaxException;
++import java.lang.reflect.Type;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.function.Predicate;
++import net.minecraft.commands.arguments.NbtPathArgument;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public class NbtPathSerializer extends ScalarSerializer<NbtPathArgument.NbtPath> {
++
++ public static final NbtPathSerializer SERIALIZER = new NbtPathSerializer();
++ private static final NbtPathArgument DUMMY_ARGUMENT = new NbtPathArgument();
++
++ private NbtPathSerializer() {
++ super(NbtPathArgument.NbtPath.class);
++ }
++
++ @Override
++ public NbtPathArgument.NbtPath deserialize(final Type type, final Object obj) throws SerializationException {
++ return fromString(obj.toString());
++ }
++
++ @Override
++ protected Object serialize(final NbtPathArgument.NbtPath item, final Predicate<Class<?>> typeSupported) {
++ return item.toString();
++ }
++
++ public static List<NbtPathArgument.NbtPath> fromString(final List<String> tags) {
++ List<NbtPathArgument.NbtPath> paths = new ArrayList<>();
++ try {
++ for (final String tag : tags) {
++ paths.add(fromString(tag));
++ }
++ } catch (SerializationException ex) {
++ SneakyThrow.sneaky(ex);
++ }
++ return List.copyOf(paths);
++ }
++
++ private static NbtPathArgument.NbtPath fromString(final String tag) throws SerializationException {
++ try {
++ return DUMMY_ARGUMENT.parse(new StringReader(tag));
++ } catch (CommandSyntaxException e) {
++ throw new SerializationException(NbtPathArgument.NbtPath.class, e);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b61935052154e76b1b8cb49868c96c52f34a41d1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
+@@ -0,0 +1,85 @@
++package io.papermc.paper.configuration.serializer;
++
++import com.google.common.collect.BiMap;
++import com.google.common.collect.ImmutableBiMap;
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.configuration.serializer.collections.MapSerializer;
++import io.papermc.paper.util.ObfHelper;
++import java.lang.reflect.Type;
++import java.util.List;
++import java.util.Map;
++import java.util.function.Predicate;
++import net.minecraft.network.protocol.Packet;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++@SuppressWarnings("Convert2Diamond")
++public final class PacketClassSerializer extends ScalarSerializer<Class<? extends Packet<?>>> implements MapSerializer.WriteBack {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final TypeToken<Class<? extends Packet<?>>> TYPE = new TypeToken<Class<? extends Packet<?>>>() {};
++ private static final List<String> SUBPACKAGES = List.of("game", "handshake", "login", "status");
++ private static final BiMap<String, String> MOJANG_TO_OBF;
++
++ static {
++ final ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.builder();
++ final @Nullable Map<String, ObfHelper.ClassMapping> classMappingMap = ObfHelper.INSTANCE.mappingsByMojangName();
++ if (classMappingMap != null) {
++ classMappingMap.forEach((mojMap, classMapping) -> {
++ if (mojMap.startsWith("net.minecraft.network.protocol.")) {
++ builder.put(classMapping.mojangName(), classMapping.obfName());
++ }
++ });
++ }
++ MOJANG_TO_OBF = builder.build();
++ }
++
++ public PacketClassSerializer() {
++ super(TYPE);
++ }
++
++ @SuppressWarnings("unchecked")
++ @Override
++ public Class<? extends Packet<?>> deserialize(final Type type, final Object obj) throws SerializationException {
++ Class<?> packetClass = null;
++ for (final String subpackage : SUBPACKAGES) {
++ final String fullClassName = "net.minecraft.network.protocol." + subpackage + "." + obj;
++ try {
++ packetClass = Class.forName(fullClassName);
++ break;
++ } catch (final ClassNotFoundException ex) {
++ final String spigotClassName = MOJANG_TO_OBF.get(fullClassName);
++ if (spigotClassName != null) {
++ try {
++ packetClass = Class.forName(spigotClassName);
++ } catch (final ClassNotFoundException ignore) {}
++ }
++ }
++ }
++ if (packetClass == null || !Packet.class.isAssignableFrom(packetClass)) {
++ throw new SerializationException("Could not deserialize a packet from " + obj);
++ }
++ return (Class<? extends Packet<?>>) packetClass;
++ }
++
++ @Override
++ protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) {
++ final String name = packetClass.getName();
++ @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
++ if (mojName == null && MOJANG_TO_OBF.containsKey(name)) {
++ mojName = name;
++ }
++ if (mojName != null) {
++ int pos = mojName.lastIndexOf('.');
++ if (pos != -1 && pos != mojName.length() - 1) {
++ return mojName.substring(pos + 1);
++ }
++ }
++
++ LOGGER.error("Could not serialize {} into a mojang-mapped packet class name", packetClass);
++ return null;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..629012cb8ea8d8d81f99033794226bef19ee6c80
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java
+@@ -0,0 +1,51 @@
++package io.papermc.paper.configuration.serializer;
++
++import java.lang.reflect.Type;
++import java.util.Collections;
++import java.util.Map;
++import java.util.function.Function;
++import java.util.function.Predicate;
++import net.minecraft.util.StringRepresentable;
++import net.minecraft.world.entity.MobCategory;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public final class StringRepresentableSerializer extends ScalarSerializer<StringRepresentable> {
++ private static final Map<Type, Function<String, StringRepresentable>> TYPES = Collections.synchronizedMap(Map.ofEntries(
++ createEntry(MobCategory.class)
++ ));
++
++ public StringRepresentableSerializer() {
++ super(StringRepresentable.class);
++ }
++
++ public static boolean isValidFor(final Type type) {
++ return TYPES.containsKey(type);
++ }
++
++ private static <E extends Enum<E> & StringRepresentable> Map.Entry<Type, Function<String, @Nullable StringRepresentable>> createEntry(Class<E> type) {
++ return Map.entry(type, s -> {
++ for (E value : type.getEnumConstants()) {
++ if (value.getSerializedName().equals(s)) {
++ return value;
++ }
++ }
++ return null;
++ });
++ }
++
++ @Override
++ public StringRepresentable deserialize(Type type, Object obj) throws SerializationException {
++ Function<String, StringRepresentable> function = TYPES.get(type);
++ if (function == null) {
++ throw new SerializationException(type + " isn't registered");
++ }
++ return function.apply(obj.toString());
++ }
++
++ @Override
++ protected Object serialize(StringRepresentable item, Predicate<Class<?>> typeSupported) {
++ return item.getSerializedName();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..68ed5ad7b6f28a9fdda35e25c12a13a2619e1449
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/FastutilMapSerializer.java
+@@ -0,0 +1,91 @@
++package io.papermc.paper.configuration.serializer.collections;
++
++import io.leangen.geantyref.GenericTypeReflector;
++import io.leangen.geantyref.TypeFactory;
++import java.lang.annotation.Annotation;
++import java.lang.reflect.AnnotatedParameterizedType;
++import java.lang.reflect.AnnotatedType;
++import java.lang.reflect.ParameterizedType;
++import java.lang.reflect.Type;
++import java.util.Collections;
++import java.util.Map;
++import java.util.function.Function;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++
++@SuppressWarnings("rawtypes")
++public abstract class FastutilMapSerializer<M extends Map<?, ?>> implements TypeSerializer.Annotated<M> {
++
++ private final Function<? super Map, ? extends M> factory;
++
++ protected FastutilMapSerializer(final Function<? super Map, ? extends M> factory) {
++ this.factory = factory;
++ }
++
++ @Override
++ public M deserialize(final AnnotatedType annotatedType, final ConfigurationNode node) throws SerializationException {
++ final Map map = (Map) node.get(this.createAnnotatedMapType((AnnotatedParameterizedType) annotatedType));
++ return this.factory.apply(map == null ? Collections.emptyMap() : map);
++ }
++
++ @Override
++ public void serialize(final AnnotatedType annotatedType, final @Nullable M obj, final ConfigurationNode node) throws SerializationException {
++ if (obj == null || obj.isEmpty()) {
++ node.raw(null);
++ } else {
++ final AnnotatedType baseMapType = this.createAnnotatedMapType((AnnotatedParameterizedType) annotatedType);
++ node.set(baseMapType, obj);
++ }
++ }
++
++ private AnnotatedType createAnnotatedMapType(final AnnotatedParameterizedType type) {
++ final Type baseType = this.createBaseMapType((ParameterizedType) type.getType());
++ return GenericTypeReflector.annotate(baseType, type.getAnnotations());
++ }
++
++ protected abstract Type createBaseMapType(final ParameterizedType type);
++
++ public static final class SomethingToPrimitive<M extends Map<?, ?>> extends FastutilMapSerializer<M> {
++
++ private final Type primitiveType;
++
++ public SomethingToPrimitive(final Function<Map, ? extends M> factory, final Type primitiveType) {
++ super(factory);
++ this.primitiveType = primitiveType;
++ }
++
++ @Override
++ protected Type createBaseMapType(final ParameterizedType type) {
++ return TypeFactory.parameterizedClass(Map.class, type.getActualTypeArguments()[0], GenericTypeReflector.box(this.primitiveType));
++ }
++ }
++
++ public static final class PrimitiveToSomething<M extends Map<?, ?>> extends FastutilMapSerializer<M> {
++
++ private final Type primitiveType;
++
++ public PrimitiveToSomething(final Function<Map, ? extends M> factory, final Type primitiveType) {
++ super(factory);
++ this.primitiveType = primitiveType;
++ }
++
++ @Override
++ protected Type createBaseMapType(final ParameterizedType type) {
++ return TypeFactory.parameterizedClass(Map.class, GenericTypeReflector.box(this.primitiveType), type.getActualTypeArguments()[0]);
++ }
++ }
++
++ public static final class SomethingToSomething<M extends Map<?, ?>> extends FastutilMapSerializer<M> {
++
++ public SomethingToSomething(final Function<? super Map, ? extends M> factory) {
++ super(factory);
++ }
++
++ @Override
++ protected Type createBaseMapType(final ParameterizedType type) {
++ return TypeFactory.parameterizedClass(Map.class, type.getActualTypeArguments()[0], type.getActualTypeArguments()[1]);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6bb8304b9d98bf2ba53274a3808e607e08f0787f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java
+@@ -0,0 +1,182 @@
++package io.papermc.paper.configuration.serializer.collections;
++
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.reflect.AnnotatedType;
++import java.lang.reflect.ParameterizedType;
++import java.lang.reflect.Type;
++import java.util.Collections;
++import java.util.HashSet;
++import java.util.LinkedHashMap;
++import java.util.Map;
++import java.util.Set;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.BasicConfigurationNode;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++import org.spongepowered.configurate.serialize.TypeSerializerCollection;
++
++import static java.util.Objects.requireNonNull;
++
++/**
++ * Map serializer that does not throw errors on individual entry serialization failures.
++ */
++public class MapSerializer implements TypeSerializer.Annotated<Map<?, ?>> {
++
++ public static final TypeToken<Map<?, ?>> TYPE = new TypeToken<Map<?, ?>>() {};
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final boolean clearInvalids;
++ private final TypeSerializer<Map<?, ?>> fallback;
++
++ public MapSerializer(boolean clearInvalids) {
++ this.clearInvalids = clearInvalids;
++ this.fallback = requireNonNull(TypeSerializerCollection.defaults().get(TYPE), "Could not find default Map<?, ?> serializer");
++ }
++
++ @Retention(RetentionPolicy.RUNTIME)
++ public @interface ThrowExceptions {}
++
++ @Override
++ public Map<?, ?> deserialize(AnnotatedType annotatedType, ConfigurationNode node) throws SerializationException {
++ if (annotatedType.isAnnotationPresent(ThrowExceptions.class)) {
++ return this.fallback.deserialize(annotatedType, node);
++ }
++ final Map<Object, Object> map = new LinkedHashMap<>();
++ final Type type = annotatedType.getType();
++ if (node.isMap()) {
++ if (!(type instanceof ParameterizedType parameterizedType)) {
++ throw new SerializationException(type, "Raw types are not supported for collections");
++ }
++ if (parameterizedType.getActualTypeArguments().length != 2) {
++ throw new SerializationException(type, "Map expected two type arguments!");
++ }
++ final Type key = parameterizedType.getActualTypeArguments()[0];
++ final Type value = parameterizedType.getActualTypeArguments()[1];
++ final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key);
++ final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value);
++ if (keySerializer == null) {
++ throw new SerializationException(type, "No type serializer available for key type " + key);
++ }
++ if (valueSerializer == null) {
++ throw new SerializationException(type, "No type serializer available for value type " + value);
++ }
++
++ final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options());
++ final Set<Object> keysToClear = new HashSet<>();
++ for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
++ final @Nullable Object deserializedKey = deserialize(key, keySerializer, "key", keyNode.set(ent.getKey()), node.path());
++ final @Nullable Object deserializedValue = deserialize(value, valueSerializer, "value", ent.getValue(), ent.getValue().path());
++ if (deserializedKey == null || deserializedValue == null) {
++ continue;
++ }
++ if (keySerializer instanceof WriteBack) {
++ if (serialize(key, keySerializer, deserializedKey, "key", keyNode, node.path()) && !ent.getKey().equals(requireNonNull(keyNode.raw(), "Key must not be null!"))) {
++ keysToClear.add(ent.getKey());
++ }
++ }
++ map.put(deserializedKey, deserializedValue);
++ }
++ if (keySerializer instanceof WriteBack) { // supports cleaning keys which deserialize to the same value
++ for (Object keyToClear : keysToClear) {
++ node.node(keyToClear).raw(null);
++ }
++ }
++ }
++ return map;
++ }
++
++ private @Nullable Object deserialize(Type type, TypeSerializer<?> serializer, String mapPart, ConfigurationNode node, NodePath path) {
++ try {
++ return serializer.deserialize(type, node);
++ } catch (SerializationException ex) {
++ ex.initPath(node::path);
++ LOGGER.error("Could not deserialize {} {} into {} at {}: {}", mapPart, node.raw(), type, path, ex.rawMessage());
++ }
++ return null;
++ }
++
++ @Override
++ public void serialize(AnnotatedType annotatedType, @Nullable Map<?, ?> obj, ConfigurationNode node) throws SerializationException {
++ if (annotatedType.isAnnotationPresent(ThrowExceptions.class)) {
++ this.fallback.serialize(annotatedType, obj, node);
++ return;
++ }
++ final Type type = annotatedType.getType();
++ if (!(type instanceof ParameterizedType parameterizedType)) {
++ throw new SerializationException(type, "Raw types are not supported for collections");
++ }
++ if (parameterizedType.getActualTypeArguments().length != 2) {
++ throw new SerializationException(type, "Map expected two type arguments!");
++ }
++ final Type key = parameterizedType.getActualTypeArguments()[0];
++ final Type value = parameterizedType.getActualTypeArguments()[1];
++ final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key);
++ final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value);
++
++ if (keySerializer == null) {
++ throw new SerializationException(type, "No type serializer available for key type " + key);
++ }
++
++ if (valueSerializer == null) {
++ throw new SerializationException(type, "No type serializer available for value type " + value);
++ }
++
++ if (obj == null || obj.isEmpty()) {
++ node.set(Collections.emptyMap());
++ } else {
++ final Set<Object> unvisitedKeys;
++ if (node.empty()) {
++ node.raw(Collections.emptyMap());
++ unvisitedKeys = Collections.emptySet();
++ } else {
++ unvisitedKeys = new HashSet<>(node.childrenMap().keySet());
++ }
++ final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options());
++ for (Map.Entry<?, ?> ent : obj.entrySet()) {
++ if (!serialize(key, keySerializer, ent.getKey(), "key", keyNode, node.path())) {
++ continue;
++ }
++ final Object keyObj = requireNonNull(keyNode.raw(), "Key must not be null!");
++ final ConfigurationNode child = node.node(keyObj);
++ serialize(value, valueSerializer, ent.getValue(), "value", child, child.path());
++ unvisitedKeys.remove(keyObj);
++ }
++ if (this.clearInvalids) {
++ for (Object unusedChild : unvisitedKeys) {
++ node.removeChild(unusedChild);
++ }
++ }
++ }
++ }
++
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ private boolean serialize(Type type, TypeSerializer serializer, Object object, String mapPart, ConfigurationNode node, NodePath path) {
++ try {
++ serializer.serialize(type, object, node);
++ return true;
++ } catch (SerializationException ex) {
++ ex.initPath(node::path);
++ LOGGER.error("Could not serialize {} {} from {} at {}: {}", mapPart, object, type, path, ex.rawMessage());
++ }
++ return false;
++ }
++
++ @Override
++ public @Nullable Map<?, ?> emptyValue(AnnotatedType specificType, ConfigurationOptions options) {
++ if (specificType.isAnnotationPresent(ThrowExceptions.class)) {
++ return this.fallback.emptyValue(specificType, options);
++ }
++ return new LinkedHashMap<>();
++ }
++
++ public interface WriteBack { // marker interface
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c02b2aa2565645d64c2709310d2aba9e32fd536d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/TableSerializer.java
+@@ -0,0 +1,88 @@
++package io.papermc.paper.configuration.serializer.collections;
++
++import com.google.common.collect.HashBasedTable;
++import com.google.common.collect.ImmutableTable;
++import com.google.common.collect.Table;
++import io.leangen.geantyref.TypeFactory;
++import java.lang.reflect.ParameterizedType;
++import java.lang.reflect.Type;
++import java.util.Map;
++import java.util.Objects;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.BasicConfigurationNode;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++
++public class TableSerializer implements TypeSerializer<Table<?, ?, ?>> {
++ private static final int ROW_TYPE_ARGUMENT_INDEX = 0;
++ private static final int COLUMN_TYPE_ARGUMENT_INDEX = 1;
++ private static final int VALUE_TYPE_ARGUMENT_INDEX = 2;
++
++ @Override
++ public Table<?, ?, ?> deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
++ final Table<?, ?, ?> table = HashBasedTable.create();
++ if (!node.empty() && node.isMap()) {
++ this.deserialize0(table, (ParameterizedType) type, node);
++ }
++ return table;
++ }
++
++ @SuppressWarnings("unchecked")
++ private <R, C, V> void deserialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException {
++ final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX];
++ final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX];
++ final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX];
++
++ final TypeSerializer<R> rowKeySerializer = (TypeSerializer<R>) node.options().serializers().get(rowType);
++ if (rowKeySerializer == null) {
++ throw new SerializationException("Could not find serializer for table row type " + rowType);
++ }
++
++ final Type mapType = TypeFactory.parameterizedClass(Map.class, columnType, valueType);
++ final TypeSerializer<Map<C, V>> columnValueSerializer = (TypeSerializer<Map<C, V>>) node.options().serializers().get(mapType);
++ if (columnValueSerializer == null) {
++ throw new SerializationException("Could not find serializer for table column-value map " + type);
++ }
++
++ final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options());
++
++ for (final Object key : node.childrenMap().keySet()) {
++ final R rowKey = rowKeySerializer.deserialize(rowType, rowKeyNode.set(key));
++ final Map<C, V> map = columnValueSerializer.deserialize(mapType, node.node(rowKeyNode.raw()));
++ map.forEach((column, value) -> table.put(rowKey, column, value));
++ }
++ }
++
++ @Override
++ public void serialize(final Type type, final @Nullable Table<?, ?, ?> table, final ConfigurationNode node) throws SerializationException {
++ if (table != null) {
++ this.serialize0(table, (ParameterizedType) type, node);
++ }
++ }
++
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ private <R, C, V> void serialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException {
++ final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX];
++ final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX];
++ final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX];
++
++ final TypeSerializer rowKeySerializer = node.options().serializers().get(rowType);
++ if (rowKeySerializer == null) {
++ throw new SerializationException("Could not find a serializer for table row type " + rowType);
++ }
++
++ final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options());
++ for (final R key : table.rowKeySet()) {
++ rowKeySerializer.serialize(rowType, key, rowKeyNode.set(key));
++ final Object keyObj = Objects.requireNonNull(rowKeyNode.raw());
++ node.node(keyObj).set(TypeFactory.parameterizedClass(Map.class, columnType, valueType), table.row(key));
++ }
++ }
++
++ @Override
++ public @Nullable Table<?, ?, ?> emptyValue(Type specificType, ConfigurationOptions options) {
++ return ImmutableTable.of();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/package-info.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..78899dc1357626f993119efabf29c52396e15b2b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.serializer.collections;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/package-info.java b/src/main/java/io/papermc/paper/configuration/serializer/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..757de4562e038a2d62f276e18e86ecab7c7332bf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.serializer;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6b600a133f0689a70d1619c684c5e3c0f313b42d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java
+@@ -0,0 +1,63 @@
++package io.papermc.paper.configuration.serializer.registry;
++
++import io.leangen.geantyref.TypeToken;
++import java.lang.reflect.Type;
++import java.util.function.Predicate;
++import net.minecraft.core.Registry;
++import net.minecraft.core.RegistryAccess;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++abstract class RegistryEntrySerializer<T, R> extends ScalarSerializer<T> {
++
++ private final RegistryAccess registryAccess;
++ private final ResourceKey<? extends Registry<R>> registryKey;
++ private final boolean omitMinecraftNamespace;
++
++ protected RegistryEntrySerializer(TypeToken<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) {
++ super(type);
++ this.registryAccess = registryAccess;
++ this.registryKey = registryKey;
++ this.omitMinecraftNamespace = omitMinecraftNamespace;
++ }
++
++ protected RegistryEntrySerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) {
++ super(type);
++ this.registryAccess = registryAccess;
++ this.registryKey = registryKey;
++ this.omitMinecraftNamespace = omitMinecraftNamespace;
++ }
++
++ protected final Registry<R> registry() {
++ return this.registryAccess.lookupOrThrow(this.registryKey);
++ }
++
++ protected abstract T convertFromResourceKey(ResourceKey<R> key) throws SerializationException;
++
++ @Override
++ public final T deserialize(Type type, Object obj) throws SerializationException {
++ return this.convertFromResourceKey(this.deserializeKey(obj));
++ }
++
++ protected abstract ResourceKey<R> convertToResourceKey(T value);
++
++ @Override
++ protected final Object serialize(T item, Predicate<Class<?>> typeSupported) {
++ final ResourceKey<R> key = this.convertToResourceKey(item);
++ if (this.omitMinecraftNamespace && key.location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {
++ return key.location().getPath();
++ } else {
++ return key.location().toString();
++ }
++ }
++
++ private ResourceKey<R> deserializeKey(final Object input) throws SerializationException {
++ final ResourceLocation key = ResourceLocation.tryParse(input.toString());
++ if (key == null) {
++ throw new SerializationException("Could not create a key from " + input);
++ }
++ return ResourceKey.create(this.registryKey, key);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..76f6219eac049afef7ce03cd30d7c3232b5b9b7c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java
+@@ -0,0 +1,34 @@
++package io.papermc.paper.configuration.serializer.registry;
++
++import com.google.common.base.Preconditions;
++import io.leangen.geantyref.TypeFactory;
++import io.leangen.geantyref.TypeToken;
++import java.util.function.Function;
++import net.minecraft.core.Holder;
++import net.minecraft.core.Registry;
++import net.minecraft.core.RegistryAccess;
++import net.minecraft.resources.ResourceKey;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public final class RegistryHolderSerializer<T> extends RegistryEntrySerializer<Holder<T>, T> {
++
++ @SuppressWarnings("unchecked")
++ public RegistryHolderSerializer(TypeToken<T> typeToken, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
++ super((TypeToken<Holder<T>>) TypeToken.get(TypeFactory.parameterizedClass(Holder.class, typeToken.getType())), registryAccess, registryKey, omitMinecraftNamespace);
++ }
++
++ public RegistryHolderSerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
++ this(TypeToken.get(type), registryAccess, registryKey, omitMinecraftNamespace);
++ Preconditions.checkArgument(type.getTypeParameters().length == 0, "%s must have 0 type parameters", type);
++ }
++
++ @Override
++ protected Holder<T> convertFromResourceKey(ResourceKey<T> key) throws SerializationException {
++ return this.registry().get(key).orElseThrow(() -> new SerializationException("Missing holder in " + this.registry().key() + " with key " + key));
++ }
++
++ @Override
++ protected ResourceKey<T> convertToResourceKey(Holder<T> value) {
++ return value.unwrap().map(Function.identity(), r -> this.registry().getResourceKey(r).orElseThrow());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6831b7b72c5e1f79eff36019ca2ff56531c26df8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java
+@@ -0,0 +1,35 @@
++package io.papermc.paper.configuration.serializer.registry;
++
++import io.leangen.geantyref.TypeToken;
++import net.minecraft.core.Registry;
++import net.minecraft.core.RegistryAccess;
++import net.minecraft.resources.ResourceKey;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++/**
++ * Use {@link RegistryHolderSerializer} for datapack-configurable things.
++ */
++public final class RegistryValueSerializer<T> extends RegistryEntrySerializer<T, T> {
++
++ public RegistryValueSerializer(TypeToken<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
++ super(type, registryAccess, registryKey, omitMinecraftNamespace);
++ }
++
++ public RegistryValueSerializer(Class<T> type, final RegistryAccess registryAccess, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
++ super(type, registryAccess, registryKey, omitMinecraftNamespace);
++ }
++
++ @Override
++ protected T convertFromResourceKey(ResourceKey<T> key) throws SerializationException {
++ final T value = this.registry().getValue(key);
++ if (value == null) {
++ throw new SerializationException("Missing value in " + this.registry() + " with key " + key.location());
++ }
++ return value;
++ }
++
++ @Override
++ protected ResourceKey<T> convertToResourceKey(T value) {
++ return this.registry().getResourceKey(value).orElseThrow();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/package-info.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5185a314b555dffb6dea355dc9e2b005e5808843
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.serializer.registry;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..96e8d03bd4a4d43633a94bb251054610ac07315a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java
+@@ -0,0 +1,41 @@
++package io.papermc.paper.configuration.transformation;
++
++import io.papermc.paper.configuration.Configuration;
++import io.papermc.paper.configuration.Configurations;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++public final class Transformations {
++ private Transformations() {
++ }
++
++ public static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
++ moveFromRootAndRename(builder, key, key, parents);
++ }
++
++ public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
++ moveFromRootAndRename(builder, path(oldKey), newKey, parents);
++ }
++
++ public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final NodePath oldKey, final String newKey, final String... parents) {
++ builder.addAction(oldKey, (path, value) -> {
++ final Object[] newPath = new Object[parents.length + 1];
++ newPath[parents.length] = newKey;
++ System.arraycopy(parents, 0, newPath, 0, parents.length);
++ return newPath;
++ });
++ }
++
++ public static ConfigurationTransformation.VersionedBuilder versionedBuilder() {
++ return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.VERSION_FIELD);
++ }
++
++ @FunctionalInterface
++ public interface DefaultsAware {
++ void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7b984ec88a72a5e2581cb717d1f228b01cffbcda
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java
+@@ -0,0 +1,221 @@
++package io.papermc.paper.configuration.transformation.global;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.configuration.Configuration;
++import java.util.function.Predicate;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.minimessage.MiniMessage;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
++import org.bukkit.ChatColor;
++import org.bukkit.configuration.file.YamlConfiguration;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++public final class LegacyPaperConfig {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private LegacyPaperConfig() {
++ }
++
++ public static ConfigurationTransformation transformation(final YamlConfiguration spigotConfiguration) {
++ return ConfigurationTransformation.chain(versioned(), notVersioned(spigotConfiguration));
++ }
++
++ // Represents version transforms lifted directly from the old PaperConfig class
++ // must be run BEFORE the "settings" flatten
++ private static ConfigurationTransformation.Versioned versioned() {
++ return ConfigurationTransformation.versionedBuilder()
++ .versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
++ .addVersion(11, ConfigurationTransformation.builder().addAction(path("settings", "play-in-use-item-spam-threshold"), TransformAction.rename("incoming-packet-spam-threshold")).build())
++ .addVersion(14, ConfigurationTransformation.builder().addAction(path("settings", "spam-limiter", "tab-spam-increment"), (path, value) -> {
++ if (value.getInt() == 10) {
++ value.set(2);
++ }
++ return null;
++ }).build())
++ .addVersion(15, ConfigurationTransformation.builder().addAction(path("settings"), (path, value) -> {
++ value.node("async-chunks", "threads").set(-1);
++ return null;
++ }).build())
++ .addVersion(21, ConfigurationTransformation.builder().addAction(path("use-display-name-in-quit-message"), (path, value) -> new Object[]{"settings", "use-display-name-in-quit-message"}).build())
++ .addVersion(23, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "global-max-chunk-load-rate"), (path, value) -> {
++ if (value.getDouble() == 300.0) {
++ value.set(-1.0);
++ }
++ return null;
++ }).build())
++ .addVersion(25, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "player-max-concurrent-loads"), (path, value) -> {
++ if (value.getDouble() == 4.0) {
++ value.set(20.0);
++ }
++ return null;
++ }).build())
++ .build();
++ }
++
++ // other non-versioned transforms found in PaperConfig
++ // must be run BEFORE the "settings" flatten
++ private static ConfigurationTransformation notVersioned(final YamlConfiguration spigotConfiguration) {
++ return ConfigurationTransformation.builder()
++ .addAction(path("settings"), (path, value) -> {
++ final ConfigurationNode node = value.node("async-chunks");
++ if (node.hasChild("load-threads")) {
++ if (!node.hasChild("threads")) {
++ node.node("threads").set(node.node("load-threads").getInt());
++ }
++ node.removeChild("load-threads");
++ }
++ node.removeChild("generation");
++ node.removeChild("enabled");
++ node.removeChild("thread-per-world-generation");
++ return null;
++ })
++ .addAction(path("allow-perm-block-break-exploits"), (path, value) -> new Object[]{"settings", "unsupported-settings", "allow-permanent-block-break-exploits"})
++ .addAction(path("settings", "unsupported-settings", "allow-tnt-duplication"), TransformAction.rename("allow-piston-duplication"))
++ .addAction(path("settings", "save-player-data"), (path, value) -> {
++ final Object val = value.raw();
++ if (val instanceof Boolean bool) {
++ spigotConfiguration.set("players.disable-saving", !bool);
++ }
++ value.raw(null);
++ return null;
++ })
++ .addAction(path("settings", "log-named-entity-deaths"), (path, value) -> {
++ final Object val = value.raw();
++ if (val instanceof Boolean bool && !bool) {
++ spigotConfiguration.set("settings.log-named-deaths", false);
++ }
++ value.raw(null);
++ return null;
++ })
++ .build();
++ }
++
++ // transforms to new format with configurate
++ // must be run AFTER the "settings" flatten
++ public static ConfigurationTransformation toNewFormat() {
++ return ConfigurationTransformation.chain(
++ ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(),
++ ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build() // rename to _version to place at the top
++ );
++ }
++
++ private static ConfigurationTransformation newFormatTransformation() {
++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder()
++ .addAction(path("verbose"), TransformAction.remove()) // not needed
++ .addAction(path("unsupported-settings", "allow-headless-pistons-readme"), TransformAction.remove())
++ .addAction(path("unsupported-settings", "allow-permanent-block-break-exploits-readme"), TransformAction.remove())
++ .addAction(path("unsupported-settings", "allow-piston-duplication-readme"), TransformAction.remove())
++ .addAction(path("packet-limiter", "limits", "all"), (path, value) -> new Object[]{"packet-limiter", "all-packets"})
++ .addAction(path("packet-limiter", "limits"), (path, value) -> new Object[]{"packet-limiter", "overrides"})
++ .addAction(path("packet-limiter", "overrides", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
++ final Object keyValue = value.key();
++ if (keyValue != null && keyValue.toString().equals("PacketPlayInAutoRecipe")) { // add special cast to handle the default for moj-mapped servers that upgrade the config
++ return path.with(path.size() - 1, ServerboundPlaceRecipePacket.class.getSimpleName()).array();
++ }
++ return null;
++ }).addAction(path("loggers"), TransformAction.rename("logging"));
++
++ moveFromRootAndRename(builder, "incoming-packet-spam-threshold", "incoming-packet-threshold", "spam-limiter");
++
++ moveFromRoot(builder, "save-empty-scoreboard-teams", "scoreboards");
++ moveFromRoot(builder, "track-plugin-scoreboards", "scoreboards");
++
++ moveFromRoot(builder, "suggest-player-names-when-null-tab-completions", "commands");
++ moveFromRoot(builder, "time-command-affects-all-worlds", "commands");
++ moveFromRoot(builder, "fix-target-selector-tag-completion", "commands");
++
++ moveFromRoot(builder, "log-player-ip-addresses", "loggers");
++
++ moveFromRoot(builder, "use-display-name-in-quit-message", "messages");
++
++ moveFromRootAndRename(builder, "console-has-all-permissions", "has-all-permissions", "console");
++
++ moveFromRootAndRename(builder, "bungee-online-mode", "online-mode", "proxies", "bungee-cord");
++ moveFromRootAndRename(builder, "velocity-support", "velocity", "proxies");
++
++ moveFromRoot(builder, "book-size", "item-validation");
++ moveFromRoot(builder, "resolve-selectors-in-books", "item-validation");
++
++ moveFromRoot(builder, "enable-player-collisions", "collisions");
++ moveFromRoot(builder, "send-full-pos-for-hard-colliding-entities", "collisions");
++
++ moveFromRootAndRename(builder, "player-auto-save-rate", "rate", "player-auto-save");
++ moveFromRootAndRename(builder, "max-player-auto-save-per-tick", "max-per-tick", "player-auto-save");
++
++ moveFromRootToMisc(builder, "max-joins-per-tick");
++ moveFromRootToMisc(builder, "fix-entity-position-desync");
++ moveFromRootToMisc(builder, "load-permissions-yml-before-plugins");
++ moveFromRootToMisc(builder, "region-file-cache-size");
++ moveFromRootToMisc(builder, "use-alternative-luck-formula");
++ moveFromRootToMisc(builder, "lag-compensate-block-breaking");
++ moveFromRootToMisc(builder, "use-dimension-type-for-custom-spawners");
++
++ moveFromRoot(builder, "proxy-protocol", "proxies");
++
++ miniMessageWithTranslatable(builder, String::isBlank, "multiplayer.disconnect.authservers_down", "messages", "kick", "authentication-servers-down");
++ miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-player");
++ miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-vehicle");
++ miniMessage(builder, "messages", "kick", "connection-throttle");
++ miniMessage(builder, "messages", "no-permission");
++ miniMessageWithTranslatable(builder, Predicate.isEqual("&cSent too many packets"), Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED), "packet-limiter", "kick-message");
++
++ return builder.build();
++ }
++
++ private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final String i18nKey, final String... strPath) {
++ miniMessageWithTranslatable(builder, englishCheck, Component.translatable(i18nKey), strPath);
++ }
++ private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final Component component, final String... strPath) {
++ builder.addAction(path((Object[]) strPath), (path, value) -> {
++ final Object val = value.raw();
++ if (val != null) {
++ final String strVal = val.toString();
++ if (!englishCheck.test(strVal)) {
++ value.set(miniMessage(strVal));
++ return null;
++ }
++ }
++ value.set(MiniMessage.miniMessage().serialize(component));
++ return null;
++ });
++ }
++
++ private static void miniMessage(final ConfigurationTransformation.Builder builder, final String... strPath) {
++ builder.addAction(path((Object[]) strPath), (path, value) -> {
++ final Object val = value.raw();
++ if (val != null) {
++ value.set(miniMessage(val.toString()));
++ }
++ return null;
++ });
++ }
++
++ @SuppressWarnings("deprecation") // valid use to convert legacy string to mini-message in legacy migration
++ private static String miniMessage(final String input) {
++ return MiniMessage.miniMessage().serialize(LegacyComponentSerializer.legacySection().deserialize(ChatColor.translateAlternateColorCodes('&', input)));
++ }
++
++ private static void moveFromRootToMisc(final ConfigurationTransformation.Builder builder, final String key) {
++ moveFromRoot(builder, key, "misc");
++ }
++
++ private static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
++ moveFromRootAndRename(builder, key, key, parents);
++ }
++
++ private static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
++ builder.addAction(path(oldKey), (path, value) -> {
++ final Object[] newPath = new Object[parents.length + 1];
++ newPath[parents.length] = newKey;
++ System.arraycopy(parents, 0, newPath, 0, parents.length);
++ return newPath;
++ });
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/package-info.java b/src/main/java/io/papermc/paper/configuration/transformation/global/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..241101a25821d3e506308ef9d7edb36aecd76232
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.transformation.global;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bf5de965d26a59f7b4ba5ade3cdab35c37f42be2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/V29_LogIPs.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.configuration.transformation.global.versioned;
++
++import java.util.Properties;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.dedicated.DedicatedServer;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++public class V29_LogIPs implements TransformAction {
++
++ private static final int VERSION = 29;
++ private static final NodePath PATH = path("logging", "log-player-ip-addresses");
++ private static final V29_LogIPs INSTANCE = new V29_LogIPs();
++
++ private V29_LogIPs() {
++ }
++
++ public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
++ }
++
++ @Override
++ public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) throws ConfigurateException {
++ final DedicatedServer server = ((DedicatedServer) MinecraftServer.getServer());
++
++ final boolean val = value.getBoolean(server.settings.getProperties().logIPs);
++ server.settings.update((config) -> {
++ final Properties newProps = new Properties(config.properties);
++ newProps.setProperty("log-ips", String.valueOf(val));
++ return config.reload(server.registryAccess(), newProps, server.options);
++ });
++
++ value.raw(null);
++
++ return null;
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/package-info.java b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d9f90a01e88fb402b796a354bb3419193566b4b5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/global/versioned/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.transformation.global.versioned;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/package-info.java b/src/main/java/io/papermc/paper/configuration/transformation/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7a1220c80c3eb4d365dd317958bdca0a6c8321aa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.transformation;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d670865dc9bf75ccc5309222e8af7fc10325c5fb
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java
+@@ -0,0 +1,71 @@
++package io.papermc.paper.configuration.transformation.world;
++
++import com.mojang.logging.LogUtils;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.configuration.Configurations;
++import it.unimi.dsi.fastutil.objects.Reference2LongMap;
++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
++import net.minecraft.core.Holder;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
++import org.jspecify.annotations.Nullable;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import java.security.SecureRandom;
++import java.util.Objects;
++import java.util.Random;
++import java.util.concurrent.atomic.AtomicInteger;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++public final class FeatureSeedsGeneration implements TransformAction {
++
++ public static final String FEATURE_SEEDS_KEY = "feature-seeds";
++ public static final String GENERATE_KEY = "generate-random-seeds-for-all";
++ public static final String FEATURES_KEY = "features";
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final ResourceLocation worldKey;
++
++ private FeatureSeedsGeneration(ResourceLocation worldKey) {
++ this.worldKey = worldKey;
++ }
++
++ @Override
++ public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
++ ConfigurationNode featureNode = value.node(FEATURE_SEEDS_KEY, FEATURES_KEY);
++ final Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = Objects.requireNonNullElseGet(featureNode.get(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}), Reference2LongOpenHashMap::new);
++ final Random random = new SecureRandom();
++ AtomicInteger counter = new AtomicInteger(0);
++ MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).listElements().forEach(holder -> {
++ if (features.containsKey(holder)) {
++ return;
++ }
++
++ final long seed = random.nextLong();
++ features.put(holder, seed);
++ counter.incrementAndGet();
++ });
++ if (counter.get() > 0) {
++ LOGGER.info("Generated {} random feature seeds for {}", counter.get(), this.worldKey);
++ featureNode.raw(null);
++ featureNode.set(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}, features);
++ }
++ return null;
++ }
++
++
++ public static void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode) {
++ if (defaultsNode.node(FEATURE_SEEDS_KEY, GENERATE_KEY).getBoolean(false)) {
++ builder.addAction(path(), new FeatureSeedsGeneration(contextMap.require(Configurations.WORLD_KEY)));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7af8f613ac63f04f01e373ec80747336f744baa4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java
+@@ -0,0 +1,320 @@
++package io.papermc.paper.configuration.transformation.world;
++
++import io.papermc.paper.configuration.Configuration;
++import io.papermc.paper.configuration.WorldConfiguration;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Optional;
++import net.minecraft.core.Holder;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.world.entity.MobCategory;
++import net.minecraft.world.item.Item;
++import org.bukkit.Material;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static io.papermc.paper.configuration.transformation.Transformations.moveFromRoot;
++import static io.papermc.paper.configuration.transformation.Transformations.moveFromRootAndRename;
++import static org.spongepowered.configurate.NodePath.path;
++
++public final class LegacyPaperWorldConfig {
++
++ private LegacyPaperWorldConfig() {
++ }
++
++ public static ConfigurationTransformation transformation() {
++ return ConfigurationTransformation.chain(versioned(), notVersioned());
++ }
++
++ private static ConfigurationTransformation.Versioned versioned() {
++ return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
++ .addVersion(13, ConfigurationTransformation.builder().addAction(path("enable-old-tnt-cannon-behaviors"), TransformAction.rename("prevent-tnt-from-moving-in-water")).build())
++ .addVersion(16, ConfigurationTransformation.builder().addAction(path("use-chunk-inhabited-timer"), (path, value) -> {
++ if (!value.getBoolean(true)) {
++ value.raw(0);
++ } else {
++ value.raw(-1);
++ }
++ final Object[] newPath = path.array();
++ newPath[newPath.length - 1] = "fixed-chunk-inhabited-time";
++ return newPath;
++ }).build())
++ .addVersion(18, ConfigurationTransformation.builder().addAction(path("nether-ceiling-void-damage"), (path, value) -> {
++ if (value.getBoolean(false)) {
++ value.raw(128);
++ } else {
++ value.raw(0);
++ }
++ final Object[] newPath = path.array();
++ newPath[newPath.length - 1] = "nether-ceiling-void-damage-height";
++ return newPath;
++ }).build())
++ .addVersion(19, ConfigurationTransformation.builder()
++ .addAction(path("anti-xray", "hidden-blocks"), (path, value) -> {
++ final List<String> hiddenBlocks = value.getList(String.class);
++ if (hiddenBlocks != null) {
++ hiddenBlocks.remove("lit_redstone_ore");
++ }
++ return null;
++ })
++ .addAction(path("anti-xray", "replacement-blocks"), (path, value) -> {
++ final List<String> replacementBlocks = value.getList(String.class);
++ if (replacementBlocks != null) {
++ final int index = replacementBlocks.indexOf("planks");
++ if (index != -1) {
++ replacementBlocks.set(index, "oak_planks");
++ }
++ }
++ value.raw(replacementBlocks);
++ return null;
++ }).build())
++ .addVersion(20, ConfigurationTransformation.builder().addAction(path("baby-zombie-movement-speed"), TransformAction.rename("baby-zombie-movement-modifier")).build())
++ .addVersion(22, ConfigurationTransformation.builder().addAction(path("per-player-mob-spawns"), (path, value) -> {
++ value.raw(true);
++ return null;
++ }).build())
++ .addVersion(24,
++ ConfigurationTransformation.builder()
++ .addAction(path("spawn-limits", "monsters"), TransformAction.rename("monster"))
++ .addAction(path("spawn-limits", "animals"), TransformAction.rename("creature"))
++ .addAction(path("spawn-limits", "water-animals"), TransformAction.rename("water_creature"))
++ .addAction(path("spawn-limits", "water-ambient"), TransformAction.rename("water_ambient"))
++ .build(),
++ ConfigurationTransformation.builder().addAction(path("despawn-ranges"), (path, value) -> {
++ final int softDistance = value.node("soft").getInt(32);
++ final int hardDistance = value.node("hard").getInt(128);
++ value.node("soft").raw(null);
++ value.node("hard").raw(null);
++ for (final MobCategory category : MobCategory.values()) {
++ if (softDistance != 32) {
++ value.node(category.getName(), "soft").raw(softDistance);
++ }
++ if (hardDistance != 128) {
++ value.node(category.getName(), "hard").raw(hardDistance);
++ }
++ }
++ return null;
++ }).build()
++ )
++ .addVersion(26, ConfigurationTransformation.builder().addAction(path("alt-item-despawn-rate", "items", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
++ String itemName = path.get(path.size() - 1).toString();
++ final Optional<Holder.Reference<Item>> item = BuiltInRegistries.ITEM.get(ResourceKey.create(Registries.ITEM, ResourceLocation.parse(itemName.toLowerCase(Locale.ROOT))));
++ if (item.isEmpty()) {
++ itemName = Material.valueOf(itemName).getKey().getKey();
++ }
++ final Object[] newPath = path.array();
++ newPath[newPath.length - 1] = itemName;
++ return newPath;
++ }).build())
++ .addVersion(27, ConfigurationTransformation.builder().addAction(path("use-faster-eigencraft-redstone"), (path, value) -> {
++ final WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation = value.getBoolean(false) ? WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT : WorldConfiguration.Misc.RedstoneImplementation.VANILLA;
++ value.set(redstoneImplementation);
++ final Object[] newPath = path.array();
++ newPath[newPath.length - 1] = "redstone-implementation";
++ return newPath;
++ }).build())
++ .build();
++ }
++
++ // other transformations found in PaperWorldConfig that aren't versioned
++ private static ConfigurationTransformation notVersioned() {
++ return ConfigurationTransformation.builder()
++ .addAction(path("treasure-maps-return-already-discovered"), (path, value) -> {
++ boolean prevValue = value.getBoolean(false);
++ value.node("villager-trade").set(prevValue);
++ value.node("loot-tables").set(prevValue);
++ return path.with(path.size() - 1, "treasure-maps-find-already-discovered").array();
++ })
++ .addAction(path("alt-item-despawn-rate", "items"), (path, value) -> {
++ if (value.isMap()) {
++ Map<String, Integer> rebuild = new HashMap<>();
++ value.childrenMap().forEach((key, node) -> {
++ String itemName = key.toString();
++ final Optional<Holder.Reference<Item>> itemHolder = BuiltInRegistries.ITEM.get(ResourceKey.create(Registries.ITEM, ResourceLocation.parse(itemName.toLowerCase(Locale.ROOT))));
++ final String item;
++ if (itemHolder.isEmpty()) {
++ final Material bukkitMat = Material.matchMaterial(itemName);
++ item = bukkitMat != null ? bukkitMat.getKey().getKey() : null;
++ } else {
++ item = itemHolder.get().unwrapKey().orElseThrow().location().getPath();
++ }
++ if (item != null) {
++ rebuild.put(item, node.getInt());
++ }
++ });
++ value.set(rebuild);
++ }
++ return null;
++ })
++ .build();
++ }
++
++ public static ConfigurationTransformation toNewFormat() {
++ return ConfigurationTransformation.chain(ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(), ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build());
++ }
++
++ private static ConfigurationTransformation newFormatTransformation() {
++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder()
++ .addAction(path("verbose"), TransformAction.remove()); // not needed
++
++ moveFromRoot(builder, "anti-xray", "anticheat");
++
++ moveFromRootAndRename(builder, "armor-stands-do-collision-entity-lookups", "do-collision-entity-lookups", "entities", "armor-stands");
++ moveFromRootAndRename(builder, "armor-stands-tick", "tick", "entities", "armor-stands");
++
++ moveFromRoot(builder, "auto-save-interval", "chunks");
++ moveFromRoot(builder, "delay-chunk-unloads-by", "chunks");
++ moveFromRoot(builder, "entity-per-chunk-save-limit", "chunks");
++ moveFromRoot(builder, "fixed-chunk-inhabited-time", "chunks");
++ moveFromRoot(builder, "max-auto-save-chunks-per-tick", "chunks");
++ moveFromRoot(builder, "prevent-moving-into-unloaded-chunks", "chunks");
++
++ moveFromRoot(builder, "entities-target-with-follow-range", "entities");
++ moveFromRoot(builder, "mob-effects", "entities");
++
++ moveFromRoot(builder, "filter-nbt-data-from-spawn-eggs-and-related", "entities", "spawning");
++ moveFromGameMechanics(builder, "disable-mob-spawner-spawn-egg-transformation", "entities", "spawning");
++ moveFromRoot(builder, "per-player-mob-spawns", "entities", "spawning");
++ moveFromGameMechanics(builder, "scan-for-legacy-ender-dragon", "entities", "spawning");
++ moveFromRoot(builder, "spawn-limits", "entities", "spawning");
++ moveFromRoot(builder, "despawn-ranges", "entities", "spawning");
++ moveFromRoot(builder, "wateranimal-spawn-height", "entities", "spawning");
++ builder.addAction(path("slime-spawn-height", "swamp-biome"), TransformAction.rename("surface-biome"));
++ moveFromRoot(builder, "slime-spawn-height", "entities", "spawning");
++ moveFromRoot(builder, "wandering-trader", "entities", "spawning");
++ moveFromRoot(builder, "all-chunks-are-slime-chunks", "entities", "spawning");
++ moveFromRoot(builder, "skeleton-horse-thunder-spawn-chance", "entities", "spawning");
++ moveFromRoot(builder, "iron-golems-can-spawn-in-air", "entities", "spawning");
++ moveFromRoot(builder, "alt-item-despawn-rate", "entities", "spawning");
++ moveFromRoot(builder, "count-all-mobs-for-spawning", "entities", "spawning");
++ moveFromRoot(builder, "creative-arrow-despawn-rate", "entities", "spawning");
++ moveFromRoot(builder, "non-player-arrow-despawn-rate", "entities", "spawning");
++ moveFromRoot(builder, "monster-spawn-max-light-level", "entities", "spawning");
++
++
++ moveFromRootAndRename(builder, "duplicate-uuid-saferegen-delete-range", "safe-regen-delete-range", "entities", "spawning", "duplicate-uuid");
++
++ moveFromRoot(builder, "baby-zombie-movement-modifier", "entities", "behavior");
++ moveFromRoot(builder, "disable-creeper-lingering-effect", "entities", "behavior");
++ moveFromRoot(builder, "door-breaking-difficulty", "entities", "behavior");
++ moveFromGameMechanics(builder, "disable-chest-cat-detection", "entities", "behavior");
++ moveFromGameMechanics(builder, "disable-player-crits", "entities", "behavior");
++ moveFromRoot(builder, "experience-merge-max-value", "entities", "behavior");
++ moveFromRoot(builder, "mobs-can-always-pick-up-loot", "entities", "behavior");
++ moveFromGameMechanics(builder, "nerf-pigmen-from-nether-portals", "entities", "behavior");
++ moveFromRoot(builder, "parrots-are-unaffected-by-player-movement", "entities", "behavior");
++ moveFromRoot(builder, "phantoms-do-not-spawn-on-creative-players", "entities", "behavior");
++ moveFromRoot(builder, "phantoms-only-attack-insomniacs", "entities", "behavior");
++ moveFromRoot(builder, "piglins-guard-chests", "entities", "behavior");
++ moveFromRoot(builder, "spawner-nerfed-mobs-should-jump", "entities", "behavior");
++ moveFromRoot(builder, "zombie-villager-infection-chance", "entities", "behavior");
++ moveFromRoot(builder, "zombies-target-turtle-eggs", "entities", "behavior");
++ moveFromRoot(builder, "ender-dragons-death-always-places-dragon-egg", "entities", "behavior");
++ moveFromGameMechanicsAndRename(builder, "disable-pillager-patrols", "disable", "game-mechanics", "pillager-patrols");
++ moveFromGameMechanics(builder, "pillager-patrols", "entities", "behavior");
++ moveFromRoot(builder, "should-remove-dragon", "entities", "behavior");
++
++ moveFromRootAndRename(builder, "map-item-frame-cursor-limit", "item-frame-cursor-limit", "maps");
++ moveFromRootAndRename(builder, "map-item-frame-cursor-update-interval", "item-frame-cursor-update-interval", "maps");
++
++ moveFromRootAndRename(builder, "mob-spawner-tick-rate", "mob-spawner", "tick-rates");
++ moveFromRootAndRename(builder, "container-update-tick-rate", "container-update", "tick-rates");
++ moveFromRootAndRename(builder, "grass-spread-tick-rate", "grass-spread", "tick-rates");
++
++ moveFromRoot(builder, "allow-non-player-entities-on-scoreboards", "scoreboards");
++ moveFromRoot(builder, "use-vanilla-world-scoreboard-name-coloring", "scoreboards");
++
++ moveFromRoot(builder, "disable-thunder", "environment");
++ moveFromRoot(builder, "disable-ice-and-snow", "environment");
++ moveFromRoot(builder, "optimize-explosions", "environment");
++ moveFromRoot(builder, "disable-explosion-knockback", "environment");
++ moveFromRoot(builder, "frosted-ice", "environment");
++ moveFromRoot(builder, "disable-teleportation-suffocation-check", "environment");
++ moveFromRoot(builder, "portal-create-radius", "environment");
++ moveFromRoot(builder, "portal-search-radius", "environment");
++ moveFromRoot(builder, "portal-search-vanilla-dimension-scaling", "environment");
++ moveFromRootAndRename(builder, "enable-treasure-maps", "enabled", "environment", "treasure-maps");
++ moveFromRootAndRename(builder, "treasure-maps-find-already-discovered", "find-already-discovered", "environment", "treasure-maps");
++ moveFromRoot(builder, "water-over-lava-flow-speed", "environment");
++ moveFromRoot(builder, "nether-ceiling-void-damage-height", "environment");
++
++ moveFromRoot(builder, "keep-spawn-loaded", "spawn");
++ moveFromRoot(builder, "keep-spawn-loaded-range", "spawn");
++ moveFromRoot(builder, "allow-using-signs-inside-spawn-protection", "spawn");
++
++ moveFromRoot(builder, "max-entity-collisions", "collisions");
++ moveFromRoot(builder, "allow-vehicle-collisions", "collisions");
++ moveFromRoot(builder, "fix-climbing-bypassing-cramming-rule", "collisions");
++ moveFromRoot(builder, "only-players-collide", "collisions");
++ moveFromRoot(builder, "allow-player-cramming-damage", "collisions");
++
++ moveFromRoot(builder, "falling-block-height-nerf", "fixes");
++ moveFromRoot(builder, "fix-items-merging-through-walls", "fixes");
++ moveFromRoot(builder, "prevent-tnt-from-moving-in-water", "fixes");
++ moveFromRoot(builder, "remove-corrupt-tile-entities", "fixes");
++ moveFromRoot(builder, "split-overstacked-loot", "fixes");
++ moveFromRoot(builder, "tnt-entity-height-nerf", "fixes");
++ moveFromRoot(builder, "fix-wither-targeting-bug", "fixes");
++ moveFromGameMechanics(builder, "disable-unloaded-chunk-enderpearl-exploit", "fixes");
++ moveFromGameMechanics(builder, "fix-curing-zombie-villager-discount-exploit", "fixes");
++
++ builder.addAction(path("fishing-time-range", "MaximumTicks"), TransformAction.rename("maximum"));
++ builder.addAction(path("fishing-time-range", "MinimumTicks"), TransformAction.rename("minimum"));
++
++ builder.addAction(path("generator-settings", "flat-bedrock"), (path, value) -> new Object[]{"environment", "generate-flat-bedrock"});
++ builder.addAction(path("generator-settings"), TransformAction.remove());
++
++ builder.addAction(path("game-mechanics", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> new Object[]{"misc", path.array()[1]});
++ builder.addAction(path("game-mechanics"), TransformAction.remove());
++
++ builder.addAction(path("feature-seeds", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
++ final String key = path.array()[path.size() - 1].toString();
++ if (!"generate-random-seeds-for-all".equals(key)) {
++ return new Object[]{"feature-seeds", "features", key};
++ }
++ return null;
++ });
++
++ builder.addAction(path("duplicate-uuid-resolver"), (path, value) -> {
++ final WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode duplicateUUIDMode = switch (value.require(String.class)) {
++ case "regen", "regenerate", "saferegen", "saferegenerate" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN;
++ case "remove", "delete" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE;
++ case "silent", "nothing" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.NOTHING;
++ default -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN;
++ };
++ value.set(duplicateUUIDMode);
++ return new Object[]{"entities", "spawning", "duplicate-uuid", "mode"};
++ });
++
++ builder.addAction(path("redstone-implementation"), (path, value) -> {
++ if ("alternate-current".equalsIgnoreCase(value.require(String.class))) {
++ value.set("alternate_current");
++ }
++ return new Object[]{"misc", "redstone-implementation"};
++ });
++
++ moveToMisc(builder, "light-queue-size");
++ moveToMisc(builder, "update-pathfinding-on-block-update");
++ moveToMisc(builder, "show-sign-click-command-failure-msgs-to-player");
++ moveToMisc(builder, "max-leash-distance");
++
++ return builder.build();
++ }
++
++ private static void moveToMisc(final ConfigurationTransformation.Builder builder, String... key) {
++ moveFromRootAndRename(builder, path((Object[]) key), key[key.length - 1], "misc");
++ }
++
++ private static void moveFromGameMechanics(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
++ moveFromGameMechanicsAndRename(builder, key, key, parents);
++ }
++
++ private static void moveFromGameMechanicsAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
++ moveFromRootAndRename(builder, path("game-mechanics", oldKey), newKey, parents);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/package-info.java b/src/main/java/io/papermc/paper/configuration/transformation/world/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0db4d187f780a0cf90c9c2936d1f33415d004137
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.transformation.world;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a1e8ce5407f2c5f188b2ce2d768512d3d42ad64b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V29_ZeroWorldHeight.java
+@@ -0,0 +1,49 @@
++package io.papermc.paper.configuration.transformation.world.versioned;
++
++import io.papermc.paper.configuration.type.number.IntOr;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++/**
++ * Several configurations that set a y-level used '0' as the "disabled" value.
++ * Since 0 is now a valid value, they need to be updated.
++ */
++public final class V29_ZeroWorldHeight implements TransformAction {
++
++ private static final int VERSION = 29;
++
++ private static final String FIXES_KEY = "fixes";
++ private static final String FALLING_BLOCK_HEIGHT_NERF_KEY = "falling-block-height-nerf";
++ private static final String TNT_ENTITY_HEIGHT_NERF_KEY = "tnt-entity-height-nerf";
++
++ private static final String ENVIRONMENT_KEY = "environment";
++ private static final String NETHER_CEILING_VOID_DAMAGE_HEIGHT_KEY = "nether-ceiling-void-damage-height";
++
++ private static final V29_ZeroWorldHeight INSTANCE = new V29_ZeroWorldHeight();
++
++ private V29_ZeroWorldHeight() {
++ }
++
++ public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
++ final ConfigurationTransformation transformation = ConfigurationTransformation.builder()
++ .addAction(path(FIXES_KEY, FALLING_BLOCK_HEIGHT_NERF_KEY), INSTANCE)
++ .addAction(path(FIXES_KEY, TNT_ENTITY_HEIGHT_NERF_KEY), INSTANCE)
++ .addAction(path(ENVIRONMENT_KEY, NETHER_CEILING_VOID_DAMAGE_HEIGHT_KEY), INSTANCE)
++ .build();
++ builder.addVersion(VERSION, transformation);
++ }
++
++ @Override
++ public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
++ if (value.getInt() == 0) {
++ value.set(IntOr.Disabled.DISABLED);
++ }
++ return null;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d08b65234192d5b639cead675114f64bf1f409c4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V30_RenameFilterNbtFromSpawnEgg.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.configuration.transformation.world.versioned;
++
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++
++import static org.spongepowered.configurate.NodePath.path;
++import static org.spongepowered.configurate.transformation.TransformAction.rename;
++
++/**
++ * The {@code filter-nbt-data-from-spawn-eggs-and-related} setting had nothing
++ * to do with spawn eggs, and was just filtering bad falling blocks.
++ */
++public final class V30_RenameFilterNbtFromSpawnEgg {
++
++ private static final int VERSION = 30;
++ private static final NodePath OLD_PATH = path("entities", "spawning", "filter-nbt-data-from-spawn-eggs-and-related");
++ private static final String NEW_PATH = "filter-bad-tile-entity-nbt-from-falling-blocks";
++
++ private V30_RenameFilterNbtFromSpawnEgg() {
++ }
++
++ public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(OLD_PATH, rename(NEW_PATH)).build());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cb05980256f20df7b3c30e33eaa2c3185f2a38f8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/V31_SpawnLoadedRangeToGameRule.java
+@@ -0,0 +1,55 @@
++package io.papermc.paper.configuration.transformation.world.versioned;
++
++import io.papermc.paper.configuration.Configurations;
++import net.minecraft.world.level.GameRules;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++import org.spongepowered.configurate.transformation.TransformAction;
++
++import static org.spongepowered.configurate.NodePath.path;
++
++public final class V31_SpawnLoadedRangeToGameRule implements TransformAction {
++
++ private static final int VERSION = 31;
++ private static final String SPAWN = "spawn";
++ private static final String KEEP_SPAWN_LOADED_RANGE = "keep-spawn-loaded-range";
++ private static final String KEEP_SPAWN_LOADED = "keep-spawn-loaded";
++
++ private final GameRules gameRules;
++ private final ConfigurationNode defaultsNode;
++
++ private V31_SpawnLoadedRangeToGameRule(final GameRules gameRules, final ConfigurationNode defaultsNode) {
++ this.gameRules = gameRules;
++ this.defaultsNode = defaultsNode;
++ }
++
++ @Override
++ public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) {
++ final ConfigurationNode worldSpawnNode = value.node(SPAWN);
++ final ConfigurationNode worldLoadedNode = worldSpawnNode.node(KEEP_SPAWN_LOADED);
++ final boolean keepLoaded = worldLoadedNode.getBoolean(this.defaultsNode.node(SPAWN, KEEP_SPAWN_LOADED).getBoolean());
++ worldLoadedNode.raw(null);
++ final ConfigurationNode worldRangeNode = worldSpawnNode.node(KEEP_SPAWN_LOADED_RANGE);
++ final int range = worldRangeNode.getInt(this.defaultsNode.node(SPAWN, KEEP_SPAWN_LOADED_RANGE).getInt());
++ worldRangeNode.raw(null);
++ if (worldSpawnNode.empty()) {
++ worldSpawnNode.raw(null);
++ }
++ if (!keepLoaded) {
++ this.gameRules.getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(0, null);
++ } else {
++ this.gameRules.getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(range, null);
++ }
++ return null;
++ }
++
++ public static void apply(final ConfigurationTransformation.VersionedBuilder builder, final Configurations.ContextMap contextMap, final @Nullable ConfigurationNode defaultsNode) {
++ if (defaultsNode != null) {
++ builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(path(), new V31_SpawnLoadedRangeToGameRule(contextMap.require(Configurations.GAME_RULES), defaultsNode)).build());
++ } else {
++ builder.addVersion(VERSION, ConfigurationTransformation.empty()); // increment version of default world config
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/package-info.java b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..087aa63ae612aaabf4c161c56c78d47ef5b591d3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/transformation/world/versioned/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.transformation.world.versioned;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1e73f51b7f6d06a1e86b150b001f90dd179b8ec8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java
+@@ -0,0 +1,52 @@
++package io.papermc.paper.configuration.type;
++
++import java.lang.reflect.Type;
++import java.util.Locale;
++import java.util.function.Predicate;
++import org.apache.commons.lang3.BooleanUtils;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public record BooleanOrDefault(@Nullable Boolean value) {
++ private static final String DEFAULT_VALUE = "default";
++ public static final BooleanOrDefault USE_DEFAULT = new BooleanOrDefault(null);
++ public static final ScalarSerializer<BooleanOrDefault> SERIALIZER = new Serializer();
++
++ public boolean or(boolean fallback) {
++ return this.value == null ? fallback : this.value;
++ }
++
++ private static final class Serializer extends ScalarSerializer<BooleanOrDefault> {
++ Serializer() {
++ super(BooleanOrDefault.class);
++ }
++
++ @Override
++ public BooleanOrDefault deserialize(Type type, Object obj) throws SerializationException {
++ if (obj instanceof String string) {
++ if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
++ return USE_DEFAULT;
++ }
++ try {
++ return new BooleanOrDefault(BooleanUtils.toBoolean(string.toLowerCase(Locale.ROOT), "true", "false"));
++ } catch (IllegalArgumentException ex) {
++ throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'", ex);
++ }
++ } else if (obj instanceof Boolean bool) {
++ return new BooleanOrDefault(bool);
++ }
++ throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'");
++ }
++
++ @Override
++ protected Object serialize(BooleanOrDefault item, Predicate<Class<?>> typeSupported) {
++ final Boolean value = item.value;
++ if (value != null) {
++ return value.toString();
++ } else {
++ return DEFAULT_VALUE;
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/DespawnRange.java b/src/main/java/io/papermc/paper/configuration/type/DespawnRange.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7779edcbbce36d7da177a92807dac73fbe24c9fa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/DespawnRange.java
+@@ -0,0 +1,109 @@
++package io.papermc.paper.configuration.type;
++
++import io.papermc.paper.configuration.type.number.IntOr;
++import java.lang.reflect.Type;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++
++/*
++(x/a)^2 + (y/b)^2 + (z/c)^2 < 1
++a == c
++ac = horizontal limit
++b = vertical limit
++x^2/ac^2 + y^2/b^2 + z^2/ac^2 < 1
++(x^2 + z^2)/ac^2 + y^2/b^2 < 1
++x^2 + z^2 + (y^2 * (ac^2/b^2)) < ac^2
++ */
++public final class DespawnRange {
++
++ public static final TypeSerializer<DespawnRange> SERIALIZER = new Serializer();
++
++ private final IntOr.Default horizontalLimit;
++ private final IntOr.Default verticalLimit;
++ private final boolean wasDefinedViaLongSyntax;
++
++ // cached values
++ private double preComputedHorizontalLimitSquared; // ac^2
++ private double preComputedHorizontalLimitSquaredOverVerticalLimitSquared; // ac^2/b^2
++ private int preComputedVanillaDefaultLimit;
++
++ public DespawnRange(final IntOr.Default generalLimit) {
++ this(generalLimit, generalLimit, false);
++ }
++
++ public DespawnRange(final IntOr.Default horizontalLimit, final IntOr.Default verticalLimit, final boolean wasDefinedViaLongSyntax) {
++ this.horizontalLimit = horizontalLimit;
++ this.verticalLimit = verticalLimit;
++ this.wasDefinedViaLongSyntax = wasDefinedViaLongSyntax;
++ }
++
++ public void preComputed(int defaultDistanceLimit, String identifier) throws SerializationException {
++ if (this.verticalLimit.or(defaultDistanceLimit) <= 0) {
++ throw new SerializationException("Vertical limit must be greater than 0 for " + identifier);
++ }
++ if (this.horizontalLimit.or(defaultDistanceLimit) <= 0) {
++ throw new SerializationException("Horizontal limit must be greater than 0 for " + identifier);
++ }
++ this.preComputedVanillaDefaultLimit = defaultDistanceLimit;
++ this.preComputedHorizontalLimitSquared = Math.pow(this.horizontalLimit.or(defaultDistanceLimit), 2);
++ if (!this.horizontalLimit.isDefined() && !this.verticalLimit.isDefined()) {
++ this.preComputedHorizontalLimitSquaredOverVerticalLimitSquared = 1.0;
++ } else {
++ this.preComputedHorizontalLimitSquaredOverVerticalLimitSquared = this.preComputedHorizontalLimitSquared / Math.pow(this.verticalLimit.or(defaultDistanceLimit), 2);
++ }
++ }
++
++ public boolean shouldDespawn(final Shape shape, final double dxSqr, final double dySqr, final double dzSqr, final double dy) {
++ if (shape == Shape.ELLIPSOID) {
++ return dxSqr + dzSqr + (dySqr * this.preComputedHorizontalLimitSquaredOverVerticalLimitSquared) > this.preComputedHorizontalLimitSquared;
++ } else {
++ return dxSqr + dzSqr > this.preComputedHorizontalLimitSquared || dy > this.verticalLimit.or(this.preComputedVanillaDefaultLimit);
++ }
++ }
++
++ public boolean wasDefinedViaLongSyntax() {
++ return this.wasDefinedViaLongSyntax;
++ }
++
++ public enum Shape {
++ CYLINDER, ELLIPSOID
++ }
++
++ static final class Serializer implements TypeSerializer<DespawnRange> {
++
++ public static final String HORIZONTAL = "horizontal";
++ public static final String VERTICAL = "vertical";
++
++ @Override
++ public DespawnRange deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
++ if (node.hasChild(HORIZONTAL) && node.hasChild(VERTICAL)) {
++ return new DespawnRange(
++ node.node(HORIZONTAL).require(IntOr.Default.class),
++ node.node(VERTICAL).require(IntOr.Default.class),
++ true
++ );
++ } else if (node.hasChild(HORIZONTAL) || node.hasChild(VERTICAL)) {
++ throw new SerializationException(node, DespawnRange.class, "Expected both horizontal and vertical despawn ranges to be defined");
++ } else {
++ return new DespawnRange(node.require(IntOr.Default.class));
++ }
++ }
++
++ @Override
++ public void serialize(final Type type, final @Nullable DespawnRange despawnRange, final ConfigurationNode node) throws SerializationException {
++ if (despawnRange == null) {
++ node.raw(null);
++ return;
++ }
++
++ if (despawnRange.wasDefinedViaLongSyntax()) {
++ node.node(HORIZONTAL).set(despawnRange.horizontalLimit);
++ node.node(VERTICAL).set(despawnRange.verticalLimit);
++ } else {
++ node.set(despawnRange.verticalLimit);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/Duration.java b/src/main/java/io/papermc/paper/configuration/type/Duration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ad1c77388da868b61d99dd8d7ab272bf6f7142a9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/Duration.java
+@@ -0,0 +1,96 @@
++package io.papermc.paper.configuration.type;
++
++import java.lang.reflect.Type;
++import java.util.Objects;
++import java.util.function.Predicate;
++import java.util.regex.Pattern;
++import org.jspecify.annotations.Nullable;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public final class Duration {
++
++ private static final Pattern SPACE = Pattern.compile(" ");
++ private static final Pattern NOT_NUMERIC = Pattern.compile("[^-\\d.]");
++ public static final ScalarSerializer<Duration> SERIALIZER = new Serializer();
++
++ private final long seconds;
++ private final String value;
++
++ private Duration(String value) {
++ this.value = value;
++ this.seconds = getSeconds(value);
++ }
++
++ public long seconds() {
++ return this.seconds;
++ }
++
++ public long ticks() {
++ return this.seconds * 20;
++ }
++
++ public String value() {
++ return this.value;
++ }
++
++ @Override
++ public boolean equals(@Nullable Object o) {
++ if (this == o) return true;
++ if (o == null || getClass() != o.getClass()) return false;
++ Duration duration = (Duration) o;
++ return seconds == duration.seconds && this.value.equals(duration.value);
++ }
++
++ @Override
++ public int hashCode() {
++ return Objects.hash(this.seconds, this.value);
++ }
++
++ @Override
++ public String toString() {
++ return "Duration{" +
++ "seconds=" + this.seconds +
++ ", value='" + this.value + '\'' +
++ '}';
++ }
++
++ public static Duration of(String time) {
++ return new Duration(time);
++ }
++
++ private static int getSeconds(String str) {
++ str = SPACE.matcher(str).replaceAll("");
++ final char unit = str.charAt(str.length() - 1);
++ str = NOT_NUMERIC.matcher(str).replaceAll("");
++ double num;
++ try {
++ num = Double.parseDouble(str);
++ } catch (Exception e) {
++ num = 0D;
++ }
++ switch (unit) {
++ case 'd': num *= (double) 60*60*24; break;
++ case 'h': num *= (double) 60*60; break;
++ case 'm': num *= (double) 60; break;
++ default: case 's': break;
++ }
++ return (int) num;
++ }
++
++ static final class Serializer extends ScalarSerializer<Duration> {
++ private Serializer() {
++ super(Duration.class);
++ }
++
++ @Override
++ public Duration deserialize(Type type, Object obj) throws SerializationException {
++ return new Duration(obj.toString());
++ }
++
++ @Override
++ protected Object serialize(Duration item, Predicate<Class<?>> typeSupported) {
++ return item.value();
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java b/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3f17e75e08e1cb4359b96a78c5b8d5284c484e43
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/DurationOrDisabled.java
+@@ -0,0 +1,54 @@
++package io.papermc.paper.configuration.type;
++
++import java.lang.reflect.Type;
++import java.util.Optional;
++import java.util.function.Predicate;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
++public final class DurationOrDisabled {
++ private static final String DISABLE_VALUE = "disabled";
++ public static final DurationOrDisabled USE_DISABLED = new DurationOrDisabled(Optional.empty());
++ public static final ScalarSerializer<DurationOrDisabled> SERIALIZER = new Serializer();
++
++ private Optional<Duration> value;
++
++ public DurationOrDisabled(final Optional<Duration> value) {
++ this.value = value;
++ }
++
++ public Optional<Duration> value() {
++ return this.value;
++ }
++
++ public void value(final Optional<Duration> value) {
++ this.value = value;
++ }
++
++ public Duration or(final Duration fallback) {
++ return this.value.orElse(fallback);
++ }
++
++ private static final class Serializer extends ScalarSerializer<DurationOrDisabled> {
++ Serializer() {
++ super(DurationOrDisabled.class);
++ }
++
++ @Override
++ public DurationOrDisabled deserialize(final Type type, final Object obj) throws SerializationException {
++ if (obj instanceof final String string) {
++ if (DISABLE_VALUE.equalsIgnoreCase(string)) {
++ return USE_DISABLED;
++ }
++ return new DurationOrDisabled(Optional.of(Duration.SERIALIZER.deserialize(string)));
++ }
++ throw new SerializationException(obj + "(" + type + ") is not a duration or '" + DISABLE_VALUE + "'");
++ }
++
++ @Override
++ protected Object serialize(final DurationOrDisabled item, final Predicate<Class<?>> typeSupported) {
++ return item.value.map(Duration::value).orElse(DISABLE_VALUE);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/EngineMode.java b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7f8b685762f59049fde88e8d1bc10e1504916010
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java
+@@ -0,0 +1,37 @@
++package io.papermc.paper.configuration.type;
++
++import io.papermc.paper.configuration.serializer.EngineModeSerializer;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++
++public enum EngineMode {
++
++ HIDE(1, "hide ores"), OBFUSCATE(2, "obfuscate"), OBFUSCATE_LAYER(3, "obfuscate layer");
++
++ public static final ScalarSerializer<EngineMode> SERIALIZER = new EngineModeSerializer();
++
++ private final int id;
++ private final String description;
++
++ EngineMode(int id, String description) {
++ this.id = id;
++ this.description = description;
++ }
++
++ public static EngineMode valueOf(int id) {
++ for (EngineMode engineMode : values()) {
++ if (engineMode.getId() == id) {
++ return engineMode;
++ }
++ }
++
++ throw new IllegalArgumentException("No enum constant with id " + id);
++ }
++
++ public int getId() {
++ return id;
++ }
++
++ public String getDescription() {
++ return description;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..24763d3d270c29c95e0b3e85111145234f660a62
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java
+@@ -0,0 +1,38 @@
++package io.papermc.paper.configuration.type.fallback;
++
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.util.Map;
++import java.util.OptionalInt;
++import java.util.Set;
++
++public class ArrowDespawnRate extends FallbackValue.Int {
++
++ ArrowDespawnRate(Map<ContextKey<?>, Object> context, Object value) throws SerializationException {
++ super(context, fromObject(value));
++ }
++
++ private ArrowDespawnRate(Map<ContextKey<?>, Object> context) {
++ super(context, OptionalInt.empty());
++ }
++
++ @Override
++ protected OptionalInt process(int value) {
++ return Util.negToDef(value);
++ }
++
++ @Override
++ public Set<ContextKey<?>> required() {
++ return Set.of(FallbackValue.SPIGOT_WORLD_CONFIG);
++ }
++
++ @Override
++ protected int fallback() {
++ return this.get(FallbackValue.SPIGOT_WORLD_CONFIG).arrowDespawnRate;
++ }
++
++ public static ArrowDespawnRate def(SpigotWorldConfig spigotConfig) {
++ return new ArrowDespawnRate(FallbackValue.SPIGOT_WORLD_CONFIG.singleton(spigotConfig));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0f2765b2edc63c11ba3c57ff55c536054826a995
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java
+@@ -0,0 +1,39 @@
++package io.papermc.paper.configuration.type.fallback;
++
++import net.minecraft.server.MinecraftServer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.util.Map;
++import java.util.OptionalInt;
++import java.util.Set;
++import java.util.function.Supplier;
++
++public class AutosavePeriod extends FallbackValue.Int {
++
++ AutosavePeriod(Map<ContextKey<?>, Object> contextMap, Object value) throws SerializationException {
++ super(contextMap, fromObject(value));
++ }
++
++ private AutosavePeriod(Map<ContextKey<?>, Object> contextMap) {
++ super(contextMap, OptionalInt.empty());
++ }
++
++ @Override
++ protected OptionalInt process(int value) {
++ return Util.negToDef(value);
++ }
++
++ @Override
++ protected Set<ContextKey<?>> required() {
++ return Set.of(FallbackValue.MINECRAFT_SERVER);
++ }
++
++ @Override
++ protected int fallback() {
++ return this.get(FallbackValue.MINECRAFT_SERVER).get().autosavePeriod;
++ }
++
++ public static AutosavePeriod def() {
++ return new AutosavePeriod(FallbackValue.MINECRAFT_SERVER.singleton(MinecraftServer::getServer));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a3a1d398d783c37914fb6d646e11361afee687b8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java
+@@ -0,0 +1,102 @@
++package io.papermc.paper.configuration.type.fallback;
++
++import com.google.common.base.Preconditions;
++import net.minecraft.server.MinecraftServer;
++import org.apache.commons.lang3.math.NumberUtils;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.util.Map;
++import java.util.Objects;
++import java.util.OptionalInt;
++import java.util.Set;
++import java.util.function.Supplier;
++
++@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
++public sealed abstract class FallbackValue permits FallbackValue.Int {
++
++ private static final String DEFAULT_VALUE = "default";
++ static final ContextKey<SpigotWorldConfig> SPIGOT_WORLD_CONFIG = new ContextKey<>("SpigotWorldConfig");
++ static final ContextKey<Supplier<MinecraftServer>> MINECRAFT_SERVER = new ContextKey<>("MinecraftServer");
++
++ private final Map<ContextKey<?>, Object> contextMap;
++
++ protected FallbackValue(Map<ContextKey<?>, Object> contextMap) {
++ for (ContextKey<?> contextKey : this.required()) {
++ Preconditions.checkArgument(contextMap.containsKey(contextKey), contextMap + " is missing " + contextKey);
++ }
++ this.contextMap = contextMap;
++ }
++
++ protected abstract String serialize();
++
++ protected abstract Set<ContextKey<?>> required();
++
++ @SuppressWarnings("unchecked")
++ protected <T> T get(ContextKey<T> contextKey) {
++ return (T) Objects.requireNonNull(this.contextMap.get(contextKey), "Missing " + contextKey);
++ }
++
++ public non-sealed abstract static class Int extends FallbackValue {
++
++ private final OptionalInt value;
++
++ Int(Map<ContextKey<?>, Object> contextMap, OptionalInt value) {
++ super(contextMap);
++ if (value.isEmpty()) {
++ this.value = value;
++ } else {
++ this.value = this.process(value.getAsInt());
++ }
++ }
++
++ public int value() {
++ return value.orElseGet(this::fallback);
++ }
++
++ @Override
++ protected final String serialize() {
++ return value.isPresent() ? String.valueOf(this.value.getAsInt()) : DEFAULT_VALUE;
++ }
++
++ protected OptionalInt process(int value) {
++ return OptionalInt.of(value);
++ }
++
++ protected abstract int fallback();
++
++ protected static OptionalInt fromObject(Object obj) throws SerializationException {
++ if (obj instanceof OptionalInt optionalInt) {
++ return optionalInt;
++ } else if (obj instanceof String string) {
++ if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
++ return OptionalInt.empty();
++ }
++ if (NumberUtils.isParsable(string)) {
++ return OptionalInt.of(Integer.parseInt(string));
++ }
++ } else if (obj instanceof Integer num) {
++ return OptionalInt.of(num);
++ }
++ throw new SerializationException(obj + " is not a integer or '" + DEFAULT_VALUE + "'");
++ }
++ }
++
++ static class ContextKey<T> {
++
++ private final String name;
++
++ ContextKey(String name) {
++ this.name = name;
++ }
++
++ @Override
++ public String toString() {
++ return this.name;
++ }
++
++ Map<ContextKey<?>, Object> singleton(T value) {
++ return Map.of(this, value);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8def3c63b146905df287779cbe2502ff89ecd4bd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java
+@@ -0,0 +1,53 @@
++package io.papermc.paper.configuration.type.fallback;
++
++import java.lang.reflect.Type;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.function.Predicate;
++import java.util.function.Supplier;
++import net.minecraft.server.MinecraftServer;
++import org.spigotmc.SpigotWorldConfig;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import static io.leangen.geantyref.GenericTypeReflector.erase;
++
++public class FallbackValueSerializer extends ScalarSerializer<FallbackValue> {
++
++ private static final Map<Class<?>, FallbackCreator<?>> REGISTRY = new HashMap<>();
++
++ static {
++ REGISTRY.put(ArrowDespawnRate.class, ArrowDespawnRate::new);
++ REGISTRY.put(AutosavePeriod.class, AutosavePeriod::new);
++ }
++
++ FallbackValueSerializer(Map<FallbackValue.ContextKey<?>, Object> contextMap) {
++ super(FallbackValue.class);
++ this.contextMap = contextMap;
++ }
++
++ @FunctionalInterface
++ private interface FallbackCreator<T extends FallbackValue> {
++ T create(Map<FallbackValue.ContextKey<?>, Object> context, Object value) throws SerializationException;
++ }
++
++ private final Map<FallbackValue.ContextKey<?>, Object> contextMap;
++
++ @Override
++ public FallbackValue deserialize(Type type, Object obj) throws SerializationException {
++ final FallbackCreator<?> creator = REGISTRY.get(erase(type));
++ if (creator == null) {
++ throw new SerializationException(type + " does not have a FallbackCreator registered");
++ }
++ return creator.create(this.contextMap, obj);
++ }
++
++ @Override
++ protected Object serialize(FallbackValue item, Predicate<Class<?>> typeSupported) {
++ return item.serialize();
++ }
++
++ public static FallbackValueSerializer create(SpigotWorldConfig config, Supplier<MinecraftServer> server) {
++ return new FallbackValueSerializer(Map.of(FallbackValue.SPIGOT_WORLD_CONFIG, config, FallbackValue.MINECRAFT_SERVER, server));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..70cc7b45e7355f6c8476a74a070f1266e4cca189
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java
+@@ -0,0 +1,10 @@
++package io.papermc.paper.configuration.type.fallback;
++
++import java.util.OptionalInt;
++
++final class Util {
++
++ static OptionalInt negToDef(int value) {
++ return value < 0 ? OptionalInt.empty() : OptionalInt.of(value);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/package-info.java b/src/main/java/io/papermc/paper/configuration/type/fallback/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7daf9e0ec8f35b68373d4f025ec2366ab110e22e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/fallback/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.type.fallback;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..31068170086aeac51a2adb952b19672e875ba528
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java
+@@ -0,0 +1,11 @@
++package io.papermc.paper.configuration.type.number;
++
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++
++@Retention(RetentionPolicy.RUNTIME)
++@Target(ElementType.FIELD)
++public @interface BelowZeroToEmpty {
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0e7205e6ba9b207082c8c530142f0b832dcd242d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java
+@@ -0,0 +1,74 @@
++package io.papermc.paper.configuration.type.number;
++
++import com.google.common.base.Preconditions;
++import java.util.OptionalDouble;
++import java.util.function.DoublePredicate;
++import java.util.function.Function;
++import java.util.function.Predicate;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++
++public interface DoubleOr {
++
++ default double or(final double fallback) {
++ return this.value().orElse(fallback);
++ }
++
++ OptionalDouble value();
++
++ default double doubleValue() {
++ return this.value().orElseThrow();
++ }
++
++ record Default(OptionalDouble value) implements DoubleOr {
++ private static final String DEFAULT_VALUE = "default";
++ public static final Default USE_DEFAULT = new Default(OptionalDouble.empty());
++ public static final ScalarSerializer<Default> SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT);
++ }
++
++ record Disabled(OptionalDouble value) implements DoubleOr {
++ private static final String DISABLED_VALUE = "disabled";
++ public static final Disabled DISABLED = new Disabled(OptionalDouble.empty());
++ public static final ScalarSerializer<Disabled> SERIALIZER = new Serializer<>(Disabled.class, Disabled::new, DISABLED_VALUE, DISABLED);
++
++ public boolean test(DoublePredicate predicate) {
++ return this.value.isPresent() && predicate.test(this.value.getAsDouble());
++ }
++
++ public boolean enabled() {
++ return this.value.isPresent();
++ }
++ }
++
++ final class Serializer<T extends DoubleOr> extends OptionalNumSerializer<T, OptionalDouble> {
++ Serializer(final Class<T> classOfT, final Function<OptionalDouble, T> factory, String emptySerializedValue, T emptyValue) {
++ super(classOfT, emptySerializedValue, emptyValue, OptionalDouble::empty, OptionalDouble::isEmpty, factory, double.class);
++ }
++
++ @Override
++ protected Object serialize(final T item, final Predicate<Class<?>> typeSupported) {
++ final OptionalDouble value = item.value();
++ if (value.isPresent()) {
++ return value.getAsDouble();
++ } else {
++ return this.emptySerializedValue;
++ }
++ }
++
++ @Override
++ protected OptionalDouble full(final String value) {
++ return OptionalDouble.of(Double.parseDouble(value));
++ }
++
++ @Override
++ protected OptionalDouble full(final Number num) {
++ return OptionalDouble.of(num.doubleValue());
++ }
++
++ @Override
++ protected boolean belowZero(final OptionalDouble value) {
++ Preconditions.checkArgument(value.isPresent());
++ return value.getAsDouble() < 0;
++ }
++ }
++}
++
+diff --git a/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..73a7b664923121daedac8f01a26253438da68119
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java
+@@ -0,0 +1,85 @@
++package io.papermc.paper.configuration.type.number;
++
++import com.google.common.base.Preconditions;
++import com.mojang.logging.LogUtils;
++import java.util.OptionalInt;
++import java.util.function.Function;
++import java.util.function.IntPredicate;
++import java.util.function.Predicate;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++
++public interface IntOr {
++
++ Logger LOGGER = LogUtils.getClassLogger();
++
++ default int or(final int fallback) {
++ return this.value().orElse(fallback);
++ }
++
++ OptionalInt value();
++
++ default boolean isDefined() {
++ return this.value().isPresent();
++ }
++
++ default int intValue() {
++ return this.value().orElseThrow();
++ }
++
++ record Default(OptionalInt value) implements IntOr {
++ private static final String DEFAULT_VALUE = "default";
++ public static final Default USE_DEFAULT = new Default(OptionalInt.empty());
++ public static final ScalarSerializer<Default> SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT);
++ }
++
++ record Disabled(OptionalInt value) implements IntOr {
++ private static final String DISABLED_VALUE = "disabled";
++ public static final Disabled DISABLED = new Disabled(OptionalInt.empty());
++ public static final ScalarSerializer<Disabled> SERIALIZER = new Serializer<>(Disabled.class, Disabled::new, DISABLED_VALUE, DISABLED);
++
++ public boolean test(IntPredicate predicate) {
++ return this.value.isPresent() && predicate.test(this.value.getAsInt());
++ }
++
++ public boolean enabled() {
++ return this.value.isPresent();
++ }
++ }
++
++ final class Serializer<T extends IntOr> extends OptionalNumSerializer<T, OptionalInt> {
++
++ private Serializer(Class<T> classOfT, Function<OptionalInt, T> factory, String emptySerializedValue, T emptyValue) {
++ super(classOfT, emptySerializedValue, emptyValue, OptionalInt::empty, OptionalInt::isEmpty, factory, int.class);
++ }
++
++ @Override
++ protected OptionalInt full(final String value) {
++ return OptionalInt.of(Integer.parseInt(value));
++ }
++
++ @Override
++ protected OptionalInt full(final Number num) {
++ if (num.intValue() != num.doubleValue() || num.intValue() != num.longValue()) {
++ LOGGER.error("{} cannot be converted to an integer without losing information", num);
++ }
++ return OptionalInt.of(num.intValue());
++ }
++
++ @Override
++ protected boolean belowZero(final OptionalInt value) {
++ Preconditions.checkArgument(value.isPresent());
++ return value.getAsInt() < 0;
++ }
++
++ @Override
++ protected Object serialize(final T item, final Predicate<Class<?>> typeSupported) {
++ final OptionalInt value = item.value();
++ if (value.isPresent()) {
++ return value.getAsInt();
++ } else {
++ return this.emptySerializedValue;
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..614aba60bb07946a144650fd3aedb31649057ae1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.configuration.type.number;
++
++import java.lang.reflect.AnnotatedType;
++import java.util.function.Function;
++import java.util.function.Predicate;
++import java.util.function.Supplier;
++import org.apache.commons.lang3.math.NumberUtils;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public abstract class OptionalNumSerializer<T, O> extends ScalarSerializer.Annotated<T> {
++
++ protected final String emptySerializedValue;
++ protected final T emptyValue;
++ private final Supplier<O> empty;
++ private final Predicate<O> isEmpty;
++ private final Function<O, T> factory;
++ private final Class<?> number;
++
++ protected OptionalNumSerializer(final Class<T> classOfT, final String emptySerializedValue, final T emptyValue, final Supplier<O> empty, final Predicate<O> isEmpty, final Function<O, T> factory, final Class<?> number) {
++ super(classOfT);
++ this.emptySerializedValue = emptySerializedValue;
++ this.emptyValue = emptyValue;
++ this.empty = empty;
++ this.isEmpty = isEmpty;
++ this.factory = factory;
++ this.number = number;
++ }
++
++ @Override
++ public final T deserialize(final AnnotatedType type, final Object obj) throws SerializationException {
++ final O value;
++ if (obj instanceof String string) {
++ if (this.emptySerializedValue.equalsIgnoreCase(string)) {
++ value = this.empty.get();
++ } else if (NumberUtils.isParsable(string)) {
++ value = this.full(string);
++ } else {
++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue));
++ }
++ } else if (obj instanceof Number num) {
++ value = this.full(num);
++ } else {
++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue));
++ }
++ if (this.isEmpty.test(value) || (type.isAnnotationPresent(BelowZeroToEmpty.class) && this.belowZero(value))) {
++ return this.emptyValue;
++ } else {
++ return this.factory.apply(value);
++ }
++ }
++
++ protected abstract O full(final String value);
++
++ protected abstract O full(final Number num);
++
++ protected abstract boolean belowZero(O value);
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/type/number/package-info.java b/src/main/java/io/papermc/paper/configuration/type/number/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a1e8c3a3922aea672bc75c810496718ea3acfe0a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/number/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.type.number;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/configuration/type/package-info.java b/src/main/java/io/papermc/paper/configuration/type/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..af8bcae3fb6aeb75350d0783599582d0d0cd3912
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/configuration/type/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.configuration.type;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
+index 6e658e00a159e9190abca0c8fd669c603b2c8029..c791b6d176090d67ecb250c6bc71c90b6c62f447 100644
+--- a/src/main/java/net/minecraft/server/Main.java
++++ b/src/main/java/net/minecraft/server/Main.java
+@@ -131,6 +131,10 @@ public class Main {
+ RegionFileVersion.configure(dedicatedserversettings.getProperties().regionFileComression);
+ Path path2 = Paths.get("eula.txt");
+ Eula eula = new Eula(path2);
++ // Paper start - load config files early for access below if needed
++ org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("bukkit-settings"));
++ org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("spigot-settings"));
++ // Paper end - load config files early for access below if needed
+
+ if (optionset.has("initSettings")) { // CraftBukkit
+ // CraftBukkit start - SPIGOT-5761: Create bukkit.yml and commands.yml if not present
+@@ -165,7 +169,7 @@ public class Main {
+ }
+
+ File file = (File) optionset.valueOf("universe"); // CraftBukkit
+- Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file);
++ Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file, optionset); // Paper - pass OptionSet to load paper config files
+ // CraftBukkit start
+ String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName);
+ LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath());
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index d9e000e01cba7e7c889a947764e9944b4e83d1a1..b6a662ee88a550836b620bb1ede349d5e4c94dfb 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -320,6 +320,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ private static final int SAMPLE_INTERVAL = 100;
+ public final double[] recentTps = new double[ 3 ];
+ // Spigot end
++ public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files
+
+ public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
+ AtomicReference<S> atomicreference = new AtomicReference();
+@@ -420,6 +421,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+ Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
+ // CraftBukkit end
++ this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
+ }
+
+ private void readScoreboard(DimensionDataStorage persistentStateManager) {
+diff --git a/src/main/java/net/minecraft/server/Services.java b/src/main/java/net/minecraft/server/Services.java
+index dfbb04800d6f1dcbb909fcdfeb1ebf1a5efa6a48..5928e5f1934b8e247ba516595018ed5c633d3b5d 100644
+--- a/src/main/java/net/minecraft/server/Services.java
++++ b/src/main/java/net/minecraft/server/Services.java
+@@ -10,16 +10,32 @@ import javax.annotation.Nullable;
+ import net.minecraft.server.players.GameProfileCache;
+ import net.minecraft.util.SignatureValidator;
+
++
+ public record Services(
+- MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache
++ MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache, @javax.annotation.Nullable io.papermc.paper.configuration.PaperConfigurations paperConfigurations // Paper - add paper configuration files
+ ) {
++ // Paper start - add paper configuration files
++ public Services(MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache) {
++ this(sessionService, servicesKeySet, profileRepository, profileCache, null);
++ }
++
++ @Override
++ public io.papermc.paper.configuration.PaperConfigurations paperConfigurations() {
++ return java.util.Objects.requireNonNull(this.paperConfigurations);
++ }
++ // Paper end - add paper configuration files
+ private static final String USERID_CACHE_FILE = "usercache.json";
+
+- public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory) {
++ public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory, joptsimple.OptionSet optionSet) throws Exception { // Paper - add optionset to load paper config files
+ MinecraftSessionService minecraftSessionService = authenticationService.createMinecraftSessionService();
+ GameProfileRepository gameProfileRepository = authenticationService.createProfileRepository();
+ GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(rootDirectory, "usercache.json"));
+- return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache);
++ // Paper start - load paper config files from cli options
++ final java.nio.file.Path legacyConfigPath = ((File) optionSet.valueOf("paper-settings")).toPath();
++ final java.nio.file.Path configDirPath = ((File) optionSet.valueOf("paper-settings-directory")).toPath();
++ io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(legacyConfigPath, configDirPath, rootDirectory.toPath(), (File) optionSet.valueOf("spigot-settings"));
++ return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache, paperConfigurations);
++ // Paper end - load paper config files from cli options
+ }
+
+ @Nullable
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index abde0c14bf0998830f1f9a7661e9eab8b35c7b85..ca095f9d6c985b066a393debc6529973a3616397 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -199,6 +199,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
+ org.spigotmc.SpigotConfig.registerCommands();
+ // Spigot end
++ // Paper start - initialize global and world-defaults configuration
++ this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess());
++ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
++ // Paper end - initialize global and world-defaults configuration
+
+ this.setPvpAllowed(dedicatedserverproperties.pvp);
+ this.setFlightAllowed(dedicatedserverproperties.allowFlight);
+diff --git a/src/main/java/net/minecraft/server/dedicated/Settings.java b/src/main/java/net/minecraft/server/dedicated/Settings.java
+index 6d89a5414f46a0c30badb4fcd25bc6cb6d18db3a..0ec3b546db0cf3858dd9cd9ea067d1d6713a8491 100644
+--- a/src/main/java/net/minecraft/server/dedicated/Settings.java
++++ b/src/main/java/net/minecraft/server/dedicated/Settings.java
+@@ -119,6 +119,7 @@ public abstract class Settings<T extends Settings<T>> {
+ try {
+ // CraftBukkit start - Don't attempt writing to file if it's read only
+ if (path.toFile().exists() && !path.toFile().canWrite()) {
++ Settings.LOGGER.warn("Can not write to file {}, skipping.", path); // Paper - log message file is read-only
+ return;
+ }
+ // CraftBukkit end
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index d11f831a567d4c4baa4b292480d8e3fc1bc70da7..67d5aaa5faa14e5ea5213efc6b24ef5b97fc17f7 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -242,7 +242,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
+ public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
+- super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env);
++ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs
+ this.pvpMode = minecraftserver.isPvpAllowed();
+ this.convertable = convertable_conversionsession;
+ this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 275e255e396e0144c600c630fbb4a29002056753..396d7425ee3754e502b23924001b00c57c24016e 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -158,6 +158,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
+ public boolean populating;
+ public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
++ // Paper start - add paper world config
++ private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
++ public io.papermc.paper.configuration.WorldConfiguration paperConfig() {
++ return this.paperConfig;
++ }
++ // Paper end - add paper world config
+
+ public final SpigotTimings.WorldTimingsHandler timings; // Spigot
+ public static BlockPos lastPhysicsProblem; // Spigot
+@@ -175,8 +181,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+
+ public abstract ResourceKey<LevelStem> getTypeKey();
+
+- protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env) {
++ protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config
+ this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
++ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+ this.generator = gen;
+ this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env);
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 46b067fdfbbdeb3c0005b37d24ae248ec2d6bb90..d5451cb1976ca3675dd19b07bd8a2d363f82db86 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -967,6 +967,7 @@ public final class CraftServer implements Server {
+ }
+
+ org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot
++ this.console.paperConfigurations.reloadConfigs(this.console);
+ for (ServerLevel world : this.console.getAllLevels()) {
+ world.serverLevelData.setDifficulty(config.difficulty);
+ world.setSpawnSettings(config.spawnMonsters);
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index af267523b60aa9076ac3c8f92ceac65a54ffbb00..153041dc3b4df33bd63a8a4765b4aa80c911e50e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -142,6 +142,19 @@ public class Main {
+ .defaultsTo(new File("spigot.yml"))
+ .describedAs("Yml file");
+ // Spigot End
++
++ // Paper start
++ acceptsAll(asList("paper-dir", "paper-settings-directory"), "Directory for Paper settings")
++ .withRequiredArg()
++ .ofType(File.class)
++ .defaultsTo(new File(io.papermc.paper.configuration.PaperConfigurations.CONFIG_DIR))
++ .describedAs("Config directory");
++ acceptsAll(asList("paper", "paper-settings"), "File for Paper settings")
++ .withRequiredArg()
++ .ofType(File.class)
++ .defaultsTo(new File("paper.yml"))
++ .describedAs("Yml file");
++ // Paper end
+ }
+ };
+
+diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
+index 038fd72710b3084c17d52d4cce087a5bd0aa3a01..e42677a14ec8e1a42747603fb4112822e326fb70 100644
+--- a/src/main/java/org/spigotmc/SpigotConfig.java
++++ b/src/main/java/org/spigotmc/SpigotConfig.java
+@@ -96,7 +96,7 @@ public class SpigotConfig
+ }
+ }
+
+- static void readConfig(Class<?> clazz, Object instance)
++ public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public
+ {
+ for ( Method method : clazz.getDeclaredMethods() )
+ {
+diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
+index a6d2ce801b236b046b94913bccf7eccfc561f35a..8bc7a9da0eb345e65f42461e2fb22731eb80790a 100644
+--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
+@@ -58,8 +58,14 @@ public class SpigotWorldConfig
+
+ public int getInt(String path, int def)
+ {
+- this.config.addDefault( "world-settings.default." + path, def );
+- return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path ) );
++ // Paper start - get int without setting default
++ return this.getInt(path, def, true);
++ }
++ public int getInt(String path, int def, boolean setDef)
++ {
++ if (setDef) this.config.addDefault( "world-settings.default." + path, def );
++ return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path, def ) );
++ // Paper end
+ }
+
+ public <T> List getList(String path, T def)
+@@ -138,14 +144,14 @@ public class SpigotWorldConfig
+ public double itemMerge;
+ private void itemMerge()
+ {
+- this.itemMerge = this.getDouble("merge-radius.item", 2.5 );
++ this.itemMerge = this.getDouble("merge-radius.item", 0.5 );
+ this.log( "Item Merge Radius: " + this.itemMerge );
+ }
+
+ public double expMerge;
+ private void expMerge()
+ {
+- this.expMerge = this.getDouble("merge-radius.exp", 3.0 );
++ this.expMerge = this.getDouble("merge-radius.exp", -1 );
+ this.log( "Experience Merge Radius: " + this.expMerge );
+ }
+
+@@ -197,7 +203,7 @@ public class SpigotWorldConfig
+
+ public int animalActivationRange = 32;
+ public int monsterActivationRange = 32;
+- public int raiderActivationRange = 48;
++ public int raiderActivationRange = 64;
+ public int miscActivationRange = 16;
+ public boolean tickInactiveVillagers = true;
+ public boolean ignoreSpectatorActivation = false;
+@@ -212,10 +218,10 @@ public class SpigotWorldConfig
+ this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation );
+ }
+
+- public int playerTrackingRange = 48;
+- public int animalTrackingRange = 48;
+- public int monsterTrackingRange = 48;
+- public int miscTrackingRange = 32;
++ public int playerTrackingRange = 128;
++ public int animalTrackingRange = 96;
++ public int monsterTrackingRange = 96;
++ public int miscTrackingRange = 96;
+ public int displayTrackingRange = 128;
+ public int otherTrackingRange = 64;
+ private void trackingRange()
+diff --git a/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0396589795da1f83ddf62426236dde9a3afa1376
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.configuration;
++
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++public final class GlobalConfigTestingBase {
++
++ public static void setupGlobalConfigForTest() {
++ //noinspection ConstantConditions
++ if (GlobalConfiguration.get() == null) {
++ ConfigurationNode node = PaperConfigurations.createForTesting();
++ try {
++ GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class);
++ GlobalConfiguration.set(globalConfiguration);
++ } catch (SerializationException e) {
++ throw new RuntimeException(e);
++ }
++ }
++ }
++}
+diff --git a/src/test/java/org/bukkit/support/DummyServerHelper.java b/src/test/java/org/bukkit/support/DummyServerHelper.java
+index 0a6ba289a94468b67d282a199250142e1e86f075..bdfa164ea21cba91b30b965d65d47112111a1209 100644
+--- a/src/test/java/org/bukkit/support/DummyServerHelper.java
++++ b/src/test/java/org/bukkit/support/DummyServerHelper.java
+@@ -91,6 +91,7 @@ public final class DummyServerHelper {
+ when(instance.getPluginManager()).thenReturn(pluginManager);
+ // Paper end - testing additions
+
++ io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper - configuration files - setup global configuration test base
+ return instance;
+ }
+ }
diff --git a/patches/server/0006-MC-Dev-fixes.patch b/patches/server/0006-MC-Dev-fixes.patch
new file mode 100644
index 0000000000..e79167b41d
--- /dev/null
+++ b/patches/server/0006-MC-Dev-fixes.patch
@@ -0,0 +1,150 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Wed, 30 Mar 2016 19:36:20 -0400
+Subject: [PATCH] MC Dev fixes
+
+
+diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
+index 078af824c2b8d7e7ac05c1c370bd630ee90b434b..56947030e423bf314f32c8dba7e841949336b8cf 100644
+--- a/src/main/java/net/minecraft/Util.java
++++ b/src/main/java/net/minecraft/Util.java
+@@ -537,7 +537,7 @@ public class Util {
+ public static <K extends Enum<K>, V> EnumMap<K, V> makeEnumMap(Class<K> enumClass, Function<K, V> mapper) {
+ EnumMap<K, V> enumMap = new EnumMap<>(enumClass);
+
+- for (K enum_ : (Enum[])enumClass.getEnumConstants()) {
++ for (K enum_ : enumClass.getEnumConstants()) { // Paper - decompile error
+ enumMap.put(enum_, mapper.apply(enum_));
+ }
+
+diff --git a/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java b/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java
+index 643bb8860962ad691b11073f6dbf406bf7ec5fb1..9b8ec1fd158f6e51779be263fd56b9119d0d9bbb 100644
+--- a/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java
++++ b/src/main/java/net/minecraft/commands/arguments/item/ItemInput.java
+@@ -78,6 +78,6 @@ public class ItemInput {
+ }
+
+ private String getItemName() {
+- return this.item.unwrapKey().map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString();
++ return this.item.unwrapKey().<Object>map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString(); // Paper - decompile fix
+ }
+ }
+diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
+index d4bff51e5fe0c76d6e3832f48067b6051b217ab7..33167226a9687ce26450d30d7eebf351709c638a 100644
+--- a/src/main/java/net/minecraft/core/BlockPos.java
++++ b/src/main/java/net/minecraft/core/BlockPos.java
+@@ -446,12 +446,12 @@ public class BlockPos extends Vec3i {
+ if (this.index == l) {
+ return this.endOfData();
+ } else {
+- int i = this.index % i;
+- int j = this.index / i;
+- int k = j % j;
+- int l = j / j;
++ int offsetX = this.index % i; // Paper - decomp fix
++ int u = this.index / i; // Paper - decomp fix
++ int offsetY = u % j; // Paper - decomp fix
++ int offsetZ = u / j; // Paper - decomp fix
+ this.index++;
+- return this.cursor.set(startX + i, startY + k, startZ + l);
++ return this.cursor.set(startX + offsetX, startY + offsetY, startZ + offsetZ); // Paper - decomp fix
+ }
+ }
+ };
+diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+index 69993496b6328a963288e11d193d36bd7decfcba..f66a2154486b6d3b5873da043e51df91cd396c72 100644
+--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -325,7 +325,7 @@ public class BuiltInRegistries {
+ Bootstrap.checkBootstrapCalled(() -> "registry " + key.location());
+ ResourceLocation resourceLocation = key.location();
+ LOADERS.put(resourceLocation, () -> initializer.run(registry));
+- WRITABLE_REGISTRY.register((ResourceKey<WritableRegistry<?>>)key, registry, RegistrationInfo.BUILT_IN);
++ WRITABLE_REGISTRY.register((ResourceKey)key, registry, RegistrationInfo.BUILT_IN); // Paper - decompile fix
+ return registry;
+ }
+
+diff --git a/src/main/java/net/minecraft/nbt/TagParser.java b/src/main/java/net/minecraft/nbt/TagParser.java
+index a614e960fcd5958ad17b679eee8a8e6926f58e62..da101bca71f4710812621b98f0a0d8cab180346a 100644
+--- a/src/main/java/net/minecraft/nbt/TagParser.java
++++ b/src/main/java/net/minecraft/nbt/TagParser.java
+@@ -253,11 +253,11 @@ public class TagParser {
+ }
+
+ if (typeReader == ByteTag.TYPE) {
+- list.add((T)((NumericTag)tag).getAsByte());
++ list.add((T)(Byte)((NumericTag)tag).getAsByte()); // Paper - decompile fix
+ } else if (typeReader == LongTag.TYPE) {
+- list.add((T)((NumericTag)tag).getAsLong());
++ list.add((T)(Long)((NumericTag)tag).getAsLong()); // Paper - decompile fix
+ } else {
+- list.add((T)((NumericTag)tag).getAsInt());
++ list.add((T)(Integer)((NumericTag)tag).getAsInt()); // Paper - decompile fix
+ }
+
+ if (!this.hasElementSeparator()) {
+diff --git a/src/main/java/net/minecraft/resources/RegistryDataLoader.java b/src/main/java/net/minecraft/resources/RegistryDataLoader.java
+index 4a9b8ec9294e82fd8da166e7582637ce19dcde7c..b9b9ec93442423e99def9b2c51aedc955a7799d5 100644
+--- a/src/main/java/net/minecraft/resources/RegistryDataLoader.java
++++ b/src/main/java/net/minecraft/resources/RegistryDataLoader.java
+@@ -74,7 +74,7 @@ import org.slf4j.Logger;
+
+ public class RegistryDataLoader {
+ private static final Logger LOGGER = LogUtils.getLogger();
+- private static final Comparator<ResourceKey<?>> ERROR_KEY_COMPARATOR = Comparator.comparing(ResourceKey::registry).thenComparing(ResourceKey::location);
++ private static final Comparator<ResourceKey<?>> ERROR_KEY_COMPARATOR = Comparator.<ResourceKey<?>, ResourceLocation>comparing(ResourceKey::registry).thenComparing(ResourceKey::location); // Paper - decompile fix
+ private static final RegistrationInfo NETWORK_REGISTRATION_INFO = new RegistrationInfo(Optional.empty(), Lifecycle.experimental());
+ private static final Function<Optional<KnownPack>, RegistrationInfo> REGISTRATION_INFO_CACHE = Util.memoize(knownPacks -> {
+ Lifecycle lifecycle = knownPacks.map(KnownPack::isVanilla).map(vanilla -> Lifecycle.stable()).orElse(Lifecycle.experimental());
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index b6a662ee88a550836b620bb1ede349d5e4c94dfb..294dc6691683b769b57635ea05b4b9e4562fa9f5 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -2031,7 +2031,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ PackRepository resourcepackrepository = this.packRepository;
+
+ Objects.requireNonNull(this.packRepository);
+- return stream.map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); // CraftBukkit - decompile error
++ return stream.<Pack>map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); // CraftBukkit - decompile error // Paper - decompile error // todo: is this needed anymore?
+ }, this).thenCompose((immutablelist) -> {
+ MultiPackResourceManager resourcemanager = new MultiPackResourceManager(PackType.SERVER_DATA, immutablelist);
+ List<Registry.PendingTags<?>> list = TagLoader.loadTagsForExistingRegistries(resourcemanager, this.registries.compositeAccess());
+diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
+index 661a6274a800ca9b91bdb809d026972d23c3b263..ea72dcb064a35bc6245bc5c94d592efedd8faf41 100644
+--- a/src/main/java/net/minecraft/util/SortedArraySet.java
++++ b/src/main/java/net/minecraft/util/SortedArraySet.java
+@@ -28,7 +28,7 @@ public class SortedArraySet<T> extends AbstractSet<T> {
+ }
+
+ public static <T extends Comparable<T>> SortedArraySet<T> create(int initialCapacity) {
+- return new SortedArraySet<>(initialCapacity, Comparator.naturalOrder());
++ return new SortedArraySet<>(initialCapacity, Comparator.<T>naturalOrder()); // Paper - decompile fix
+ }
+
+ public static <T> SortedArraySet<T> create(Comparator<T> comparator) {
+diff --git a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
+index 33170f2f1d3f030fbf342e44a1baa109a34c31a7..dde1ccca98f58200910334160f0f79eb00dd2388 100644
+--- a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
++++ b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
+@@ -349,7 +349,7 @@ public class Dolphin extends AgeableWaterCreature {
+
+ @Nullable
+ @Override
+- protected SoundEvent getDeathSound() {
++ public SoundEvent getDeathSound() { // Paper - decompile error
+ return SoundEvents.DOLPHIN_DEATH;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+index 650cc276e38b44b8b5486f53dd978439c2c25bd7..d2104e6e6ac7911bdba1cea3b9eca64930165cce 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+@@ -151,7 +151,7 @@ public class ChunkStatusTasks {
+ if (protochunk instanceof ImposterProtoChunk protochunkextension) {
+ chunk1 = protochunkextension.getWrapped();
+ } else {
+- chunk1 = new LevelChunk(worldserver, protochunk, (chunk1) -> {
++ chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
+ ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
+ });
+ generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
diff --git a/patches/server/0007-ConcurrentUtil.patch b/patches/server/0007-ConcurrentUtil.patch
new file mode 100644
index 0000000000..b285b3c6e3
--- /dev/null
+++ b/patches/server/0007-ConcurrentUtil.patch
@@ -0,0 +1,10547 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Sun, 23 Jan 2022 22:58:11 -0800
+Subject: [PATCH] ConcurrentUtil
+
+
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f84a622dc29750139ac280f480b7cd132b036287
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
+@@ -0,0 +1,1421 @@
++package ca.spottedleaf.concurrentutil.collection;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++
++import java.lang.invoke.VarHandle;
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.Iterator;
++import java.util.List;
++import java.util.NoSuchElementException;
++import java.util.Queue;
++import java.util.Spliterator;
++import java.util.Spliterators;
++import java.util.function.Consumer;
++import java.util.function.IntFunction;
++import java.util.function.Predicate;
++
++/**
++ * MT-Safe linked first in first out ordered queue.
++ *
++ * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is
++ * not any slower in lower contention reads/writes.
++ * <p>
++ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}.
++ * </p>
++ * <p><b>
++ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if
++ * they are at the head of the queue.
++ * </b></p>
++ * @param <E> Type of element in this queue.
++ */
++public class MultiThreadedQueue<E> implements Queue<E> {
++
++ protected volatile LinkedNode<E> head; /* Always non-null, high chance of being the actual head */
++
++ protected volatile LinkedNode<E> tail; /* Always non-null, high chance of being the actual tail */
++
++ /* Note that it is possible to reach head from tail. */
++
++ /* IMPL NOTE: Leave hashCode and equals to their defaults */
++
++ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class);
++ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class);
++
++ /* head */
++
++ protected final void setHeadPlain(final LinkedNode<E> newHead) {
++ HEAD_HANDLE.set(this, newHead);
++ }
++
++ protected final void setHeadOpaque(final LinkedNode<E> newHead) {
++ HEAD_HANDLE.setOpaque(this, newHead);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getHeadPlain() {
++ return (LinkedNode<E>)HEAD_HANDLE.get(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getHeadOpaque() {
++ return (LinkedNode<E>)HEAD_HANDLE.getOpaque(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getHeadAcquire() {
++ return (LinkedNode<E>)HEAD_HANDLE.getAcquire(this);
++ }
++
++ /* tail */
++
++ protected final void setTailPlain(final LinkedNode<E> newTail) {
++ TAIL_HANDLE.set(this, newTail);
++ }
++
++ protected final void setTailOpaque(final LinkedNode<E> newTail) {
++ TAIL_HANDLE.setOpaque(this, newTail);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getTailPlain() {
++ return (LinkedNode<E>)TAIL_HANDLE.get(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getTailOpaque() {
++ return (LinkedNode<E>)TAIL_HANDLE.getOpaque(this);
++ }
++
++ /**
++ * Constructs a {@code MultiThreadedQueue}, initially empty.
++ * <p>
++ * The returned object may not be published without synchronization.
++ * </p>
++ */
++ public MultiThreadedQueue() {
++ final LinkedNode<E> value = new LinkedNode<>(null, null);
++ this.setHeadPlain(value);
++ this.setTailPlain(value);
++ }
++
++ /**
++ * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}.
++ * <p>
++ * The returned object may not be published without synchronization.
++ * </p>
++ * @param collection The specified collection.
++ * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements.
++ */
++ public MultiThreadedQueue(final Iterable<? extends E> collection) {
++ final Iterator<? extends E> elements = collection.iterator();
++
++ if (!elements.hasNext()) {
++ final LinkedNode<E> value = new LinkedNode<>(null, null);
++ this.setHeadPlain(value);
++ this.setTailPlain(value);
++ return;
++ }
++
++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++ LinkedNode<E> tail = head;
++
++ while (elements.hasNext()) {
++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++ tail.setNextPlain(next);
++ tail = next;
++ }
++
++ this.setHeadPlain(head);
++ this.setTailPlain(tail);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public E remove() throws NoSuchElementException {
++ final E ret = this.poll();
++
++ if (ret == null) {
++ throw new NoSuchElementException();
++ }
++
++ return ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ * <p>
++ * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue
++ * and return {@code false} if this queue is add-blocked.
++ * </p>
++ */
++ @Override
++ public boolean add(final E element) {
++ return this.offer(element);
++ }
++
++ /**
++ * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is
++ * released from that lock and this element is added. The unlock operation and addition of the specified
++ * element is atomic.
++ * @param element The specified element.
++ * @return {@code true} if this queue previously allowed additions
++ */
++ public boolean forceAdd(final E element) {
++ final LinkedNode<E> node = new LinkedNode<>(element, null);
++
++ return !this.forceAppendList(node, node);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public E element() throws NoSuchElementException {
++ final E ret = this.peek();
++
++ if (ret == null) {
++ throw new NoSuchElementException();
++ }
++
++ return ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ * <p>
++ * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked.
++ * </p>
++ */
++ @Override
++ public boolean offer(final E element) {
++ Validate.notNull(element, "Null element");
++
++ final LinkedNode<E> node = new LinkedNode<>(element, null);
++
++ return this.appendList(node, node);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public E peek() {
++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ if (this.getHeadOpaque() == head && curr != head) {
++ this.setHeadOpaque(curr);
++ }
++ return element;
++ }
++
++ if (next == null || curr == next) {
++ return null;
++ }
++ curr = next;
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public E poll() {
++ return this.removeHead();
++ }
++
++ /**
++ * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty
++ * or the head does not match the predicate, this function returns {@code null}.
++ * <p>
++ * The predicate may be invoked multiple or no times in this call.
++ * </p>
++ * @param predicate The specified predicate.
++ * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty.
++ */
++ public E pollIf(final Predicate<E> predicate) {
++ return this.removeHead(Validate.notNull(predicate, "Null predicate"));
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void clear() {
++ //noinspection StatementWithEmptyBody
++ while (this.poll() != null);
++ }
++
++ /**
++ * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail.
++ * <p>
++ * This function is MT-Safe.
++ * </p>
++ * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions.
++ */
++ public boolean preventAdds() {
++ final LinkedNode<E> deadEnd = new LinkedNode<>(null, null);
++ deadEnd.setNextPlain(deadEnd);
++
++ if (!this.appendList(deadEnd, deadEnd)) {
++ return false;
++ }
++
++ this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */
++ return true;
++ }
++
++ /**
++ * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if
++ * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()}
++ * is that this function might perform better.
++ * <p>
++ * This function is not MT-Safe.
++ * </p>
++ */
++ public void allowAdds() {
++ LinkedNode<E> tail = this.getTailPlain();
++
++ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
++ /* Thus it is possible for an outdated tail to be set */
++ while (tail != (tail = tail.getNextPlain())) {}
++
++ tail.setNextVolatile(null);
++ }
++
++ /**
++ * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked,
++ * {@code false} otherwise.
++ * <p>
++ * This function is MT-Safe, however it should not be used with {@link #allowAdds()}.
++ * </p>
++ * @return {@code true} if the queue was previously add-locked, {@code false} otherwise.
++ */
++ public boolean tryAllowAdds() {
++ LinkedNode<E> tail = this.getTailPlain();
++
++ for (int failures = 0;;) {
++ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
++ /* Thus it is possible for an outdated tail to be set */
++ while (tail != (tail = tail.getNextAcquire())) {
++ if (tail == null) {
++ return false;
++ }
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) {
++ return true;
++ }
++
++ if (tail == null) {
++ return false;
++ }
++ ++failures;
++ }
++ }
++
++ /**
++ * Atomically adds the specified element to this queue or allows additions to the queue. If additions
++ * are not allowed, the element is not added.
++ * <p>
++ * This function is MT-Safe.
++ * </p>
++ * @param element The specified element.
++ * @return {@code true} if the queue now allows additions, {@code false} if the element was added.
++ */
++ public boolean addOrAllowAdds(final E element) {
++ Validate.notNull(element, "Null element");
++ int failures = 0;
++
++ final LinkedNode<E> append = new LinkedNode<>(element, null);
++
++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++ /* It is likely due to a cache miss caused by another write to the next field */
++ final LinkedNode<E> next = curr.getNextVolatile();
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (next == null) {
++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, append);
++
++ if (compared == null) {
++ /* Added */
++ /* Avoid CASing on tail more than we need to */
++ /* CAS to avoid setting an out-of-date tail */
++ if (this.getTailOpaque() == currTail) {
++ this.setTailOpaque(append);
++ }
++ return false; // we added
++ }
++
++ ++failures;
++ curr = compared;
++ continue;
++ } else if (next == curr) {
++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(curr, null);
++
++ if (compared == curr) {
++ return true; // we let additions through
++ }
++
++ ++failures;
++
++ if (compared != null) {
++ curr = compared;
++ }
++ continue;
++ }
++
++ if (curr == currTail) {
++ /* Tail is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to tail */
++ if (currTail == (currTail = this.getTailOpaque())) {
++ curr = next;
++ } else {
++ curr = currTail;
++ }
++ }
++ }
++ }
++
++ /**
++ * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}.
++ */
++ public boolean isAddBlocked() {
++ for (LinkedNode<E> tail = this.getTailOpaque();;) {
++ LinkedNode<E> next = tail.getNextVolatile();
++ if (next == null) {
++ return false;
++ }
++
++ if (next == tail) {
++ return true;
++ }
++
++ tail = next;
++ }
++ }
++
++ /**
++ * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
++ * head is removed.
++ * <p>
++ * This function is MT-Safe.
++ * </p>
++ * If the queue is already add-blocked and empty then no operation is performed.
++ * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns
++ * an non-null value which was the previous head of queue.
++ */
++ public E pollOrBlockAdds() {
++ int failures = 0;
++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++ final E currentVal = curr.getElementVolatile();
++ final LinkedNode<E> next = curr.getNextOpaque();
++
++ if (next == curr) {
++ return null; /* Additions are already blocked */
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (currentVal != null) {
++ if (curr.getAndSetElementVolatile(null) == null) {
++ ++failures;
++ continue;
++ }
++
++ /* "CAS" to avoid setting an out-of-date head */
++ if (this.getHeadOpaque() == head) {
++ this.setHeadOpaque(next != null ? next : curr);
++ }
++
++ return currentVal;
++ }
++
++ if (next == null) {
++ /* Try to update stale head */
++ if (curr != head && this.getHeadOpaque() == head) {
++ this.setHeadOpaque(curr);
++ }
++
++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, curr);
++
++ if (compared != null) {
++ // failed to block additions
++ curr = compared;
++ ++failures;
++ continue;
++ }
++
++ return null; /* We blocked additions */
++ }
++
++ if (head == curr) {
++ /* head is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to head */
++ if (head == (head = this.getHeadOpaque())) {
++ curr = next;
++ } else {
++ curr = head;
++ }
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean remove(final Object object) {
++ Validate.notNull(object, "Null object to remove");
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) {
++ return true;
++ }
++ }
++
++ if (next == curr || next == null) {
++ break;
++ }
++ curr = next;
++ }
++
++ return false;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean removeIf(final Predicate<? super E> filter) {
++ Validate.notNull(filter, "Null filter");
++
++ boolean ret = false;
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean removeAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ boolean ret = false;
++
++ /* Volatile is required to synchronize with the write to the first element */
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean retainAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ boolean ret = false;
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public Object[] toArray() {
++ final List<E> ret = new ArrayList<>();
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ ret.add(element);
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret.toArray();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public <T> T[] toArray(final T[] array) {
++ final List<T> ret = new ArrayList<>();
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ //noinspection unchecked
++ ret.add((T)element);
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret.toArray(array);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public <T> T[] toArray(final IntFunction<T[]> generator) {
++ Validate.notNull(generator, "Null generator");
++
++ final List<T> ret = new ArrayList<>();
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ //noinspection unchecked
++ ret.add((T)element);
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return ret.toArray(generator);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public String toString() {
++ final StringBuilder builder = new StringBuilder();
++
++ builder.append("MultiThreadedQueue: {elements: {");
++
++ int deadEntries = 0;
++ int totalEntries = 0;
++ int aliveEntries = 0;
++
++ boolean addLocked = false;
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();; ++totalEntries) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element == null) {
++ ++deadEntries;
++ } else {
++ ++aliveEntries;
++ }
++
++ if (totalEntries != 0) {
++ builder.append(", ");
++ }
++
++ builder.append(totalEntries).append(": \"").append(element).append('"');
++
++ if (next == null) {
++ break;
++ }
++ if (curr == next) {
++ addLocked = true;
++ break;
++ }
++ curr = next;
++ }
++
++ builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries)
++ .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked)
++ .append("\"}");
++
++ return builder.toString();
++ }
++
++ /**
++ * Adds all elements from the specified collection to this queue. The addition is atomic.
++ * @param collection The specified collection.
++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++ * {@code false} if the specified collection contains no elements.
++ */
++ @Override
++ public boolean addAll(final Collection<? extends E> collection) {
++ return this.addAll((Iterable<? extends E>)collection);
++ }
++
++ /**
++ * Adds all elements from the specified iterable object to this queue. The addition is atomic.
++ * @param iterable The specified iterable object.
++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++ * {@code false} if the specified iterable contains no elements.
++ */
++ public boolean addAll(final Iterable<? extends E> iterable) {
++ Validate.notNull(iterable, "Null iterable");
++
++ final Iterator<? extends E> elements = iterable.iterator();
++ if (!elements.hasNext()) {
++ return false;
++ }
++
++ /* Build a list of nodes to append */
++ /* This is an much faster due to the fact that zero additional synchronization is performed */
++
++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++ LinkedNode<E> tail = head;
++
++ while (elements.hasNext()) {
++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++ tail.setNextPlain(next);
++ tail = next;
++ }
++
++ return this.appendList(head, tail);
++ }
++
++ /**
++ * Adds all of the elements from the specified array to this queue.
++ * @param items The specified array.
++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++ * {@code false} if the specified array has a length of 0.
++ */
++ public boolean addAll(final E[] items) {
++ return this.addAll(items, 0, items.length);
++ }
++
++ /**
++ * Adds all of the elements from the specified array to this queue.
++ * @param items The specified array.
++ * @param off The offset in the array.
++ * @param len The number of items.
++ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++ * {@code false} if the specified array has a length of 0.
++ */
++ public boolean addAll(final E[] items, final int off, final int len) {
++ Validate.notNull(items, "Items may not be null");
++ Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds");
++
++ if (len == 0) {
++ return false;
++ }
++
++ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null);
++ LinkedNode<E> tail = head;
++
++ for (int i = 1; i < len; ++i) {
++ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null);
++ tail.setNextPlain(next);
++ tail = next;
++ }
++
++ return this.appendList(head, tail);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean containsAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ for (final Object element : collection) {
++ if (!this.contains(element)) {
++ return false;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public Iterator<E> iterator() {
++ return new LinkedIterator<>(this.getHeadOpaque());
++ }
++
++ /**
++ * {@inheritDoc}
++ * <p>
++ * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of
++ * the queue in its current state.
++ * </p>
++ */
++ @Override
++ public int size() {
++ int size = 0;
++
++ /* Volatile is required to synchronize with the write to the first element */
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ ++size;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return size;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean isEmpty() {
++ return this.peek() == null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean contains(final Object object) {
++ Validate.notNull(object, "Null object");
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null && (element == object || element.equals(object))) {
++ return true;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return false;
++ }
++
++ /**
++ * Finds the first element in this queue that matches the predicate.
++ * @param predicate The predicate to test elements against.
++ * @return The first element that matched the predicate, {@code null} if none matched.
++ */
++ public E find(final Predicate<E> predicate) {
++ Validate.notNull(predicate, "Null predicate");
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null && predicate.test(element)) {
++ return element;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++
++ return null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void forEach(final Consumer<? super E> action) {
++ Validate.notNull(action, "Null action");
++
++ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E element = curr.getElementPlain(); /* Likely in sync */
++
++ if (element != null) {
++ action.accept(element);
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++ }
++
++ // return true if normal addition, false if the queue previously disallowed additions
++ protected final boolean forceAppendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
++ int failures = 0;
++
++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++ /* It is likely due to a cache miss caused by another write to the next field */
++ final LinkedNode<E> next = curr.getNextVolatile();
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (next == null || next == curr) {
++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(next, head);
++
++ if (compared == next) {
++ /* Added */
++ /* Avoid CASing on tail more than we need to */
++ /* "CAS" to avoid setting an out-of-date tail */
++ if (this.getTailOpaque() == currTail) {
++ this.setTailOpaque(tail);
++ }
++ return next != curr;
++ }
++
++ ++failures;
++ curr = compared;
++ continue;
++ }
++
++ if (curr == currTail) {
++ /* Tail is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to tail */
++ if (currTail == (currTail = this.getTailOpaque())) {
++ curr = next;
++ } else {
++ curr = currTail;
++ }
++ }
++ }
++ }
++
++ // return true if successful, false otherwise
++ protected final boolean appendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
++ int failures = 0;
++
++ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++ /* It is likely due to a cache miss caused by another write to the next field */
++ final LinkedNode<E> next = curr.getNextVolatile();
++
++ if (next == curr) {
++ /* Additions are stopped */
++ return false;
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (next == null) {
++ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, head);
++
++ if (compared == null) {
++ /* Added */
++ /* Avoid CASing on tail more than we need to */
++ /* CAS to avoid setting an out-of-date tail */
++ if (this.getTailOpaque() == currTail) {
++ this.setTailOpaque(tail);
++ }
++ return true;
++ }
++
++ ++failures;
++ curr = compared;
++ continue;
++ }
++
++ if (curr == currTail) {
++ /* Tail is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to tail */
++ if (currTail == (currTail = this.getTailOpaque())) {
++ curr = next;
++ } else {
++ curr = currTail;
++ }
++ }
++ }
++ }
++
++ protected final E removeHead(final Predicate<E> predicate) {
++ int failures = 0;
++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++ // volatile here synchronizes-with writes to element
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E currentVal = curr.getElementPlain();
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (currentVal != null) {
++ if (!predicate.test(currentVal)) {
++ /* Try to update stale head */
++ if (curr != head && this.getHeadOpaque() == head) {
++ this.setHeadOpaque(curr);
++ }
++ return null;
++ }
++ if (curr.getAndSetElementVolatile(null) == null) {
++ /* Failed to get head */
++ if (curr == (curr = next) || next == null) {
++ return null;
++ }
++ ++failures;
++ continue;
++ }
++
++ /* "CAS" to avoid setting an out-of-date head */
++ if (this.getHeadOpaque() == head) {
++ this.setHeadOpaque(next != null ? next : curr);
++ }
++
++ return currentVal;
++ }
++
++ if (curr == next || next == null) {
++ /* Try to update stale head */
++ if (curr != head && this.getHeadOpaque() == head) {
++ this.setHeadOpaque(curr);
++ }
++ return null; /* End of queue */
++ }
++
++ if (head == curr) {
++ /* head is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to head */
++ if (head == (head = this.getHeadOpaque())) {
++ curr = next;
++ } else {
++ curr = head;
++ }
++ }
++ }
++ }
++
++ protected final E removeHead() {
++ int failures = 0;
++ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++ final E currentVal = curr.getElementPlain();
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (currentVal != null) {
++ if (curr.getAndSetElementVolatile(null) == null) {
++ /* Failed to get head */
++ if (curr == (curr = next) || next == null) {
++ return null;
++ }
++ ++failures;
++ continue;
++ }
++
++ /* "CAS" to avoid setting an out-of-date head */
++ if (this.getHeadOpaque() == head) {
++ this.setHeadOpaque(next != null ? next : curr);
++ }
++
++ return currentVal;
++ }
++
++ if (curr == next || next == null) {
++ /* Try to update stale head */
++ if (curr != head && this.getHeadOpaque() == head) {
++ this.setHeadOpaque(curr);
++ }
++ return null; /* End of queue */
++ }
++
++ if (head == curr) {
++ /* head is likely not up-to-date */
++ curr = next;
++ } else {
++ /* Try to update to head */
++ if (head == (head = this.getHeadOpaque())) {
++ curr = next;
++ } else {
++ curr = head;
++ }
++ }
++ }
++ }
++
++ /**
++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++ * be faster than a loop on {@link #poll()}.
++ * <p>
++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++ * {@link #clear()}, etc).
++ * Write operations are safe to be called concurrently.
++ * </p>
++ * @param consumer The consumer to accept the elements.
++ * @return The total number of elements drained.
++ */
++ public int drain(final Consumer<E> consumer) {
++ return this.drain(consumer, false, ConcurrentUtil::rethrow);
++ }
++
++ /**
++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++ * be faster than a loop on {@link #poll()}.
++ * <p>
++ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
++ * additions to the queue will fail.
++ * </p>
++ * <p>
++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++ * {@link #clear()}, etc).
++ * Write operations are safe to be called concurrently.
++ * </p>
++ * @param consumer The consumer to accept the elements.
++ * @param preventAdds Whether to prevent additions to this queue after draining.
++ * @return The total number of elements drained.
++ */
++ public int drain(final Consumer<E> consumer, final boolean preventAdds) {
++ return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow);
++ }
++
++ /**
++ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++ * be faster than a loop on {@link #poll()}.
++ * <p>
++ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
++ * additions to the queue will fail.
++ * </p>
++ * <p>
++ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++ * {@link #clear()}, {@link #remove(Object)} etc).
++ * Only write operations are safe to be called concurrently.
++ * </p>
++ * @param consumer The consumer to accept the elements.
++ * @param preventAdds Whether to prevent additions to this queue after draining.
++ * @param exceptionHandler Invoked when the consumer raises an exception.
++ * @return The total number of elements drained.
++ */
++ public int drain(final Consumer<E> consumer, final boolean preventAdds, final Consumer<Throwable> exceptionHandler) {
++ Validate.notNull(consumer, "Null consumer");
++ Validate.notNull(exceptionHandler, "Null exception handler");
++
++ /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */
++ /* This allows plain write usages instead of opaque or higher */
++ int total = 0;
++
++ final LinkedNode<E> head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */
++ LinkedNode<E> curr = head;
++
++ for (;;) {
++ /* Volatile acquires with the write to the element field */
++ final E currentVal = curr.getElementPlain();
++ LinkedNode<E> next = curr.getNextVolatile();
++
++ if (next == curr) {
++ /* Add-locked nodes always have a null value */
++ break;
++ }
++
++ if (currentVal == null) {
++ if (next == null) {
++ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
++ // failed to prevent adds, continue
++ curr = next;
++ continue;
++ } else {
++ // we're done here
++ break;
++ }
++ }
++ curr = next;
++ continue;
++ }
++
++ try {
++ consumer.accept(currentVal);
++ } catch (final Exception ex) {
++ this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */
++ curr.setElementOpaque(null); /* set here, we might re-throw */
++
++ exceptionHandler.accept(ex);
++ }
++
++ curr.setElementOpaque(null);
++
++ ++total;
++
++ if (next == null) {
++ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
++ /* Retry with next value */
++ curr = next;
++ continue;
++ }
++ break;
++ }
++
++ curr = next;
++ }
++ if (curr != head) {
++ this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */
++ }
++ return total;
++ }
++
++ @Override
++ public Spliterator<E> spliterator() { // TODO implement
++ return Spliterators.spliterator(this, Spliterator.CONCURRENT |
++ Spliterator.NONNULL | Spliterator.ORDERED);
++ }
++
++ protected static final class LinkedNode<E> {
++
++ protected volatile Object element;
++ protected volatile LinkedNode<E> next;
++
++ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
++
++ protected LinkedNode(final Object element, final LinkedNode<E> next) {
++ ELEMENT_HANDLE.set(this, element);
++ NEXT_HANDLE.set(this, next);
++ }
++
++ /* element */
++
++ @SuppressWarnings("unchecked")
++ protected final E getElementPlain() {
++ return (E)ELEMENT_HANDLE.get(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final E getElementVolatile() {
++ return (E)ELEMENT_HANDLE.getVolatile(this);
++ }
++
++ protected final void setElementPlain(final E update) {
++ ELEMENT_HANDLE.set(this, (Object)update);
++ }
++
++ protected final void setElementOpaque(final E update) {
++ ELEMENT_HANDLE.setOpaque(this, (Object)update);
++ }
++
++ protected final void setElementVolatile(final E update) {
++ ELEMENT_HANDLE.setVolatile(this, (Object)update);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final E getAndSetElementVolatile(final E update) {
++ return (E)ELEMENT_HANDLE.getAndSet(this, update);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final E compareExchangeElementVolatile(final E expect, final E update) {
++ return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update);
++ }
++
++ /* next */
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getNextPlain() {
++ return (LinkedNode<E>)NEXT_HANDLE.get(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getNextOpaque() {
++ return (LinkedNode<E>)NEXT_HANDLE.getOpaque(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getNextAcquire() {
++ return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> getNextVolatile() {
++ return (LinkedNode<E>)NEXT_HANDLE.getVolatile(this);
++ }
++
++ protected final void setNextPlain(final LinkedNode<E> next) {
++ NEXT_HANDLE.set(this, next);
++ }
++
++ protected final void setNextVolatile(final LinkedNode<E> next) {
++ NEXT_HANDLE.setVolatile(this, next);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode<E> compareExchangeNextVolatile(final LinkedNode<E> expect, final LinkedNode<E> set) {
++ return (LinkedNode<E>)NEXT_HANDLE.compareAndExchange(this, expect, set);
++ }
++ }
++
++ protected static final class LinkedIterator<E> implements Iterator<E> {
++
++ protected LinkedNode<E> curr; /* last returned by next() */
++ protected LinkedNode<E> next; /* next to return from next() */
++ protected E nextElement; /* cached to avoid a race condition with removing or polling */
++
++ protected LinkedIterator(final LinkedNode<E> start) {
++ /* setup nextElement and next */
++ for (LinkedNode<E> curr = start;;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++
++ final E element = curr.getElementPlain();
++
++ if (element != null) {
++ this.nextElement = element;
++ this.next = curr;
++ break;
++ }
++
++ if (next == null || next == curr) {
++ break;
++ }
++ curr = next;
++ }
++ }
++
++ protected final void findNext() {
++ /* only called if this.nextElement != null, which means this.next != null */
++ for (LinkedNode<E> curr = this.next;;) {
++ final LinkedNode<E> next = curr.getNextVolatile();
++
++ if (next == null || next == curr) {
++ break;
++ }
++
++ final E element = next.getElementPlain();
++
++ if (element != null) {
++ this.nextElement = element;
++ this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */
++ this.next = next;
++ return;
++ }
++ curr = next;
++ }
++
++ /* out of nodes to iterate */
++ /* keep curr for remove() calls */
++ this.next = null;
++ this.nextElement = null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean hasNext() {
++ return this.nextElement != null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public E next() {
++ final E element = this.nextElement;
++
++ if (element == null) {
++ throw new NoSuchElementException();
++ }
++
++ this.findNext();
++
++ return element;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void remove() {
++ if (this.curr == null) {
++ throw new IllegalStateException();
++ }
++
++ this.curr.setElementVolatile(null);
++ this.curr = null;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6bad6f8ecc0944d2f406924c7de7e227ff1e70fa
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java
+@@ -0,0 +1,110 @@
++package ca.spottedleaf.concurrentutil.completable;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.Cancellable;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.util.function.BiConsumer;
++
++public final class CallbackCompletable<T> {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(CallbackCompletable.class);
++
++ private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
++ private T result;
++ private Throwable throwable;
++ private volatile boolean completed;
++
++ public boolean isCompleted() {
++ return this.completed;
++ }
++
++ /**
++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++ * synchronisation
++ */
++ public T getResult() {
++ return this.result;
++ }
++
++ /**
++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++ * synchronisation
++ */
++ public Throwable getThrowable() {
++ return this.throwable;
++ }
++
++ /**
++ * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete()
++ * has already been called, returns {@code null} and does not invoke the specified consumer.
++ * @param consumer Consumer to be executed on completion
++ * @throws NullPointerException If consumer is null
++ * @return A cancellable which will control the execution of the specified consumer
++ */
++ public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
++ if (this.waiters.add(consumer)) {
++ return new CancellableImpl(consumer);
++ }
++ return null;
++ }
++
++ private void completeAllWaiters(final T result, final Throwable throwable) {
++ this.completed = true;
++ BiConsumer<T, Throwable> waiter;
++ while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
++ this.completeWaiter(waiter, result, throwable);
++ }
++ }
++
++ private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
++ try {
++ consumer.accept(result, throwable);
++ } catch (final Throwable throwable2) {
++ LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
++ }
++ }
++
++ /**
++ * Adds a waiter that will be completed asynchronously by the complete() calls. If complete()
++ * has already been called, then invokes the consumer synchronously with the completed result.
++ * @param consumer Consumer to be executed on completion
++ * @throws NullPointerException If consumer is null
++ * @return A cancellable which will control the execution of the specified consumer
++ */
++ public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
++ if (this.waiters.add(consumer)) {
++ return new CancellableImpl(consumer);
++ }
++ this.completeWaiter(consumer, this.result, this.throwable);
++ return new CancellableImpl(consumer);
++ }
++
++ public void complete(final T result) {
++ this.result = result;
++ this.completeAllWaiters(result, null);
++ }
++
++ public void completeWithThrowable(final Throwable throwable) {
++ if (throwable == null) {
++ throw new NullPointerException("Throwable cannot be null");
++ }
++ this.throwable = throwable;
++ this.completeAllWaiters(null, throwable);
++ }
++
++ private final class CancellableImpl implements Cancellable {
++
++ private final BiConsumer<T, Throwable> waiter;
++
++ private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
++ this.waiter = waiter;
++ }
++
++ @Override
++ public boolean cancel() {
++ return CallbackCompletable.this.waiters.remove(this.waiter);
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..365616439fa079017d648ed7f6ddf6950a691adf
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+@@ -0,0 +1,737 @@
++package ca.spottedleaf.concurrentutil.completable;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.CompletionException;
++import java.util.concurrent.CompletionStage;
++import java.util.concurrent.Executor;
++import java.util.concurrent.ForkJoinPool;
++import java.util.concurrent.locks.LockSupport;
++import java.util.function.BiConsumer;
++import java.util.function.BiFunction;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.Supplier;
++
++public final class Completable<T> {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
++ private static final Function<? super Throwable, ? extends Throwable> DEFAULT_EXCEPTION_HANDLER = (final Throwable thr) -> {
++ LOGGER.error("Unhandled exception during Completable operation", thr);
++ return thr;
++ };
++
++ public static Executor getDefaultExecutor() {
++ return ForkJoinPool.commonPool();
++ }
++
++ private static final Transform<?, ?> COMPLETED_STACK = new Transform<>(null, null, null, null) {
++ @Override
++ public void run() {}
++ };
++ private volatile Transform<?, T> completeStack;
++ private static final VarHandle COMPLETE_STACK_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "completeStack", Transform.class);
++
++ private static final Object NULL_MASK = new Object();
++ private volatile Object result;
++ private static final VarHandle RESULT_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "result", Object.class);
++
++ private Object getResultPlain() {
++ return (Object)RESULT_HANDLE.get(this);
++ }
++
++ private Object getResultVolatile() {
++ return (Object)RESULT_HANDLE.getVolatile(this);
++ }
++
++ private void pushStackOrRun(final Transform<?, T> push) {
++ int failures = 0;
++ for (Transform<?, T> curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.getVolatile(this);;) {
++ if (curr == COMPLETED_STACK) {
++ push.execute();
++ return;
++ }
++
++ push.next = curr;
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr == (curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.compareAndExchange(this, curr, push))) {
++ return;
++ }
++ push.next = null;
++ ++failures;
++ }
++ }
++
++ private void propagateStack() {
++ Transform<?, T> topStack = (Transform<?, T>)COMPLETE_STACK_HANDLE.getAndSet(this, COMPLETED_STACK);
++ while (topStack != null) {
++ topStack.execute();
++ topStack = topStack.next;
++ }
++ }
++
++ private static Object maskNull(final Object res) {
++ return res == null ? NULL_MASK : res;
++ }
++
++ private static Object unmaskNull(final Object res) {
++ return res == NULL_MASK ? null : res;
++ }
++
++ private static Executor checkExecutor(final Executor executor) {
++ return Validate.notNull(executor, "Executor may not be null");
++ }
++
++ public Completable() {}
++
++ private Completable(final Object complete) {
++ COMPLETE_STACK_HANDLE.set(this, COMPLETED_STACK);
++ RESULT_HANDLE.setRelease(this, complete);
++ }
++
++ public static <T> Completable<T> completed(final T value) {
++ return new Completable<>(maskNull(value));
++ }
++
++ public static <T> Completable<T> failed(final Throwable ex) {
++ Validate.notNull(ex, "Exception may not be null");
++
++ return new Completable<>(new ExceptionResult(ex));
++ }
++
++ public static <T> Completable<T> supplied(final Supplier<T> supplier) {
++ return supplied(supplier, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public static <T> Completable<T> supplied(final Supplier<T> supplier, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ try {
++ return completed(supplier.get());
++ } catch (final Throwable throwable) {
++ Throwable complete;
++ try {
++ complete = exceptionHandler.apply(throwable);
++ } catch (final Throwable thr2) {
++ throwable.addSuppressed(thr2);
++ complete = throwable;
++ }
++ return failed(complete);
++ }
++ }
++
++ public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor) {
++ return suppliedAsync(supplier, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ final Completable<T> ret = new Completable<>();
++
++ class AsyncSuppliedCompletable implements Runnable, CompletableFuture.AsynchronousCompletionTask {
++ @Override
++ public void run() {
++ try {
++ ret.complete(supplier.get());
++ } catch (final Throwable throwable) {
++ Throwable complete;
++ try {
++ complete = exceptionHandler.apply(throwable);
++ } catch (final Throwable thr2) {
++ throwable.addSuppressed(thr2);
++ complete = throwable;
++ }
++ ret.completeExceptionally(complete);
++ }
++ }
++ }
++
++ try {
++ executor.execute(new AsyncSuppliedCompletable());
++ } catch (final Throwable throwable) {
++ Throwable complete;
++ try {
++ complete = exceptionHandler.apply(throwable);
++ } catch (final Throwable thr2) {
++ throwable.addSuppressed(thr2);
++ complete = throwable;
++ }
++ ret.completeExceptionally(complete);
++ }
++
++ return ret;
++ }
++
++ private boolean completeRaw(final Object value) {
++ if ((Object)RESULT_HANDLE.getVolatile(this) != null || !(boolean)RESULT_HANDLE.compareAndSet(this, (Object)null, value)) {
++ return false;
++ }
++
++ this.propagateStack();
++ return true;
++ }
++
++ public boolean complete(final T result) {
++ return this.completeRaw(maskNull(result));
++ }
++
++ public boolean completeExceptionally(final Throwable exception) {
++ Validate.notNull(exception, "Exception may not be null");
++
++ return this.completeRaw(new ExceptionResult(exception));
++ }
++
++ public boolean isDone() {
++ return this.getResultVolatile() != null;
++ }
++
++ public boolean isNormallyComplete() {
++ return this.getResultVolatile() != null && !(this.getResultVolatile() instanceof ExceptionResult);
++ }
++
++ public boolean isExceptionallyComplete() {
++ return this.getResultVolatile() instanceof ExceptionResult;
++ }
++
++ public Throwable getException() {
++ final Object res = this.getResultVolatile();
++ if (res == null) {
++ return null;
++ }
++
++ if (!(res instanceof ExceptionResult exRes)) {
++ throw new IllegalStateException("Not completed exceptionally");
++ }
++
++ return exRes.ex;
++ }
++
++ public T getNow(final T dfl) throws CompletionException {
++ final Object res = this.getResultVolatile();
++ if (res == null) {
++ return dfl;
++ }
++
++ if (res instanceof ExceptionResult exRes) {
++ throw new CompletionException(exRes.ex);
++ }
++
++ return (T)unmaskNull(res);
++ }
++
++ public T join() throws CompletionException {
++ if (this.isDone()) {
++ return this.getNow(null);
++ }
++
++ final UnparkTransform<T> unparkTransform = new UnparkTransform<>(this, Thread.currentThread());
++
++ this.pushStackOrRun(unparkTransform);
++
++ boolean interuptted = false;
++ while (!unparkTransform.isReleasable()) {
++ try {
++ ForkJoinPool.managedBlock(unparkTransform);
++ } catch (final InterruptedException ex) {
++ interuptted = true;
++ }
++ }
++
++ if (interuptted) {
++ Thread.currentThread().interrupt();
++ }
++
++ return this.getNow(null);
++ }
++
++ public CompletableFuture<T> toFuture() {
++ final Object rawResult = this.getResultVolatile();
++ if (rawResult != null) {
++ if (rawResult instanceof ExceptionResult exRes) {
++ return CompletableFuture.failedFuture(exRes.ex);
++ } else {
++ return CompletableFuture.completedFuture((T)unmaskNull(rawResult));
++ }
++ }
++
++ final CompletableFuture<T> ret = new CompletableFuture<>();
++
++ class ToFuture implements BiConsumer<T, Throwable> {
++
++ @Override
++ public void accept(final T res, final Throwable ex) {
++ if (ex != null) {
++ ret.completeExceptionally(ex);
++ } else {
++ ret.complete(res);
++ }
++ }
++ }
++
++ this.whenComplete(new ToFuture());
++
++ return ret;
++ }
++
++ public static <T> Completable<T> fromFuture(final CompletionStage<T> stage) {
++ final Completable<T> ret = new Completable<>();
++
++ class FromFuture implements BiConsumer<T, Throwable> {
++ @Override
++ public void accept(final T res, final Throwable ex) {
++ if (ex != null) {
++ ret.completeExceptionally(ex);
++ } else {
++ ret.complete(res);
++ }
++ }
++ }
++
++ stage.whenComplete(new FromFuture());
++
++ return ret;
++ }
++
++
++ public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function) {
++ return this.thenApply(function, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<U> ret = new Completable<>();
++ this.pushStackOrRun(new ApplyTransform<>(null, this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function) {
++ return this.thenApplyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor) {
++ return this.thenApplyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<U> ret = new Completable<>();
++ this.pushStackOrRun(new ApplyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++
++ public Completable<Void> thenAccept(final Consumer<? super T> consumer) {
++ return this.thenAccept(consumer, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenAccept(final Consumer<? super T> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(consumer, "Consumer may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<Void> ret = new Completable<>();
++ this.pushStackOrRun(new AcceptTransform<>(null, this, ret, exceptionHandler, consumer));
++ return ret;
++ }
++
++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer) {
++ return this.thenAcceptAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor) {
++ return this.thenAcceptAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(consumer, "Consumer may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<Void> ret = new Completable<>();
++ this.pushStackOrRun(new AcceptTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer));
++ return ret;
++ }
++
++
++ public Completable<Void> thenRun(final Runnable run) {
++ return this.thenRun(run, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenRun(final Runnable run, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(run, "Run may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<Void> ret = new Completable<>();
++ this.pushStackOrRun(new RunTransform<>(null, this, ret, exceptionHandler, run));
++ return ret;
++ }
++
++ public Completable<Void> thenRunAsync(final Runnable run) {
++ return this.thenRunAsync(run, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenRunAsync(final Runnable run, final Executor executor) {
++ return this.thenRunAsync(run, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<Void> thenRunAsync(final Runnable run, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(run, "Run may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<Void> ret = new Completable<>();
++ this.pushStackOrRun(new RunTransform<>(checkExecutor(executor), this, ret, exceptionHandler, run));
++ return ret;
++ }
++
++
++ public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++ return this.handle(function, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<U> ret = new Completable<>();
++ this.pushStackOrRun(new HandleTransform<>(null, this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++ return this.handleAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++ final Executor executor) {
++ return this.handleAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++ final Executor executor,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<U> ret = new Completable<>();
++ this.pushStackOrRun(new HandleTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++
++ public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer) {
++ return this.whenComplete(consumer, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(consumer, "Consumer may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<T> ret = new Completable<>();
++ this.pushStackOrRun(new WhenTransform<>(null, this, ret, exceptionHandler, consumer));
++ return ret;
++ }
++
++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer) {
++ return this.whenCompleteAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor) {
++ return this.whenCompleteAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(consumer, "Consumer may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<T> ret = new Completable<>();
++ this.pushStackOrRun(new WhenTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer));
++ return ret;
++ }
++
++
++ public Completable<T> exceptionally(final Function<Throwable, ? extends T> function) {
++ return this.exceptionally(function, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> exceptionally(final Function<Throwable, ? extends T> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<T> ret = new Completable<>();
++ this.pushStackOrRun(new ExceptionallyTransform<>(null, this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function) {
++ return this.exceptionallyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor) {
++ return this.exceptionallyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++ }
++
++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ Validate.notNull(function, "Function may not be null");
++ Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++ final Completable<T> ret = new Completable<>();
++ this.pushStackOrRun(new ExceptionallyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++ return ret;
++ }
++
++ private static final class ExceptionResult {
++ public final Throwable ex;
++
++ public ExceptionResult(final Throwable ex) {
++ this.ex = ex;
++ }
++ }
++
++ private static abstract class Transform<U, T> implements Runnable, CompletableFuture.AsynchronousCompletionTask {
++
++ private Transform<?, T> next;
++
++ private final Executor executor;
++ protected final Completable<T> from;
++ protected final Completable<U> to;
++ protected final Function<? super Throwable, ? extends Throwable> exceptionHandler;
++
++ protected Transform(final Executor executor, final Completable<T> from, final Completable<U> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++ this.executor = executor;
++ this.from = from;
++ this.to = to;
++ this.exceptionHandler = exceptionHandler;
++ }
++
++ // force interface call to become virtual call
++ @Override
++ public abstract void run();
++
++ protected void failed(final Throwable throwable) {
++ Throwable complete;
++ try {
++ complete = this.exceptionHandler.apply(throwable);
++ } catch (final Throwable thr2) {
++ throwable.addSuppressed(thr2);
++ complete = throwable;
++ }
++ this.to.completeExceptionally(complete);
++ }
++
++ public void execute() {
++ if (this.executor == null) {
++ this.run();
++ return;
++ }
++
++ try {
++ this.executor.execute(this);
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class ApplyTransform<U, T> extends Transform<U, T> {
++
++ private final Function<? super T, ? extends U> function;
++
++ public ApplyTransform(final Executor executor, final Completable<T> from, final Completable<U> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final Function<? super T, ? extends U> function) {
++ super(executor, from, to, exceptionHandler);
++ this.function = function;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.to.completeExceptionally(exRes.ex);
++ } else {
++ this.to.complete(this.function.apply((T)unmaskNull(result)));
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class AcceptTransform<T> extends Transform<Void, T> {
++ private final Consumer<? super T> consumer;
++
++ public AcceptTransform(final Executor executor, final Completable<T> from, final Completable<Void> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final Consumer<? super T> consumer) {
++ super(executor, from, to, exceptionHandler);
++ this.consumer = consumer;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.to.completeExceptionally(exRes.ex);
++ } else {
++ this.consumer.accept((T)unmaskNull(result));
++ this.to.complete(null);
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class RunTransform<T> extends Transform<Void, T> {
++ private final Runnable run;
++
++ public RunTransform(final Executor executor, final Completable<T> from, final Completable<Void> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final Runnable run) {
++ super(executor, from, to, exceptionHandler);
++ this.run = run;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.to.completeExceptionally(exRes.ex);
++ } else {
++ this.run.run();
++ this.to.complete(null);
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class HandleTransform<U, T> extends Transform<U, T> {
++
++ private final BiFunction<? super T, ? super Throwable, ? extends U> function;
++
++ public HandleTransform(final Executor executor, final Completable<T> from, final Completable<U> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++ super(executor, from, to, exceptionHandler);
++ this.function = function;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.to.complete(this.function.apply(null, exRes.ex));
++ } else {
++ this.to.complete(this.function.apply((T)unmaskNull(result), null));
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class WhenTransform<T> extends Transform<T, T> {
++
++ private final BiConsumer<? super T, ? super Throwable> consumer;
++
++ public WhenTransform(final Executor executor, final Completable<T> from, final Completable<T> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final BiConsumer<? super T, ? super Throwable> consumer) {
++ super(executor, from, to, exceptionHandler);
++ this.consumer = consumer;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.consumer.accept(null, exRes.ex);
++ this.to.completeExceptionally(exRes.ex);
++ } else {
++ final T unmasked = (T)unmaskNull(result);
++ this.consumer.accept(unmasked, null);
++ this.to.complete(unmasked);
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class ExceptionallyTransform<T> extends Transform<T, T> {
++ private final Function<Throwable, ? extends T> function;
++
++ public ExceptionallyTransform(final Executor executor, final Completable<T> from, final Completable<T> to,
++ final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++ final Function<Throwable, ? extends T> function) {
++ super(executor, from, to, exceptionHandler);
++ this.function = function;
++ }
++
++ @Override
++ public void run() {
++ final Object result = this.from.getResultPlain();
++ try {
++ if (result instanceof ExceptionResult exRes) {
++ this.to.complete(this.function.apply(exRes.ex));
++ } else {
++ this.to.complete((T)unmaskNull(result));
++ }
++ } catch (final Throwable throwable) {
++ this.failed(throwable);
++ }
++ }
++ }
++
++ private static final class UnparkTransform<T> extends Transform<Void, T> implements ForkJoinPool.ManagedBlocker {
++
++ private volatile Thread thread;
++
++ public UnparkTransform(final Completable<T> from, final Thread target) {
++ super(null, from, null, null);
++ this.thread = target;
++ }
++
++ @Override
++ public void run() {
++ final Thread t = this.thread;
++ this.thread = null;
++ LockSupport.unpark(t);
++ }
++
++ @Override
++ public boolean block() throws InterruptedException {
++ while (!this.isReleasable()) {
++ if (Thread.interrupted()) {
++ throw new InterruptedException();
++ }
++ LockSupport.park(this);
++ }
++
++ return true;
++ }
++
++ @Override
++ public boolean isReleasable() {
++ return this.thread == null;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..11449056361bb6c5a055f543cdd135c4113757c6
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
+@@ -0,0 +1,14 @@
++package ca.spottedleaf.concurrentutil.executor;
++
++/**
++ * Interface specifying that something can be cancelled.
++ */
++public interface Cancellable {
++
++ /**
++ * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function
++ * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only
++ * when this function successfully stops this task from being completed will it return {@code true}.
++ */
++ public boolean cancel();
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..17cbaee1e89bd3f6d905e640d20d0119ab0570a0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java
+@@ -0,0 +1,271 @@
++package ca.spottedleaf.concurrentutil.executor;
++
++import ca.spottedleaf.concurrentutil.util.Priority;
++
++public interface PrioritisedExecutor {
++
++ /**
++ * Returns the number of tasks that have been scheduled are pending to be scheduled.
++ */
++ public long getTotalTasksScheduled();
++
++ /**
++ * Returns the number of tasks that have been executed.
++ */
++ public long getTotalTasksExecuted();
++
++ /**
++ * Generates the next suborder id.
++ * @return The next suborder id.
++ */
++ public long generateNextSubOrder();
++
++ /**
++ * Executes the next available task.
++ * <p>
++ * If there is a task with priority {@link Priority#BLOCKING} available, then that such task is executed.
++ * </p>
++ * <p>
++ * If there is a task with priority {@link Priority#IDLE} available then that task is only executed
++ * when there are no other tasks available with a higher priority.
++ * </p>
++ * <p>
++ * If there are no tasks that have priority {@link Priority#BLOCKING} or {@link Priority#IDLE}, then
++ * this function will be biased to execute tasks that have higher priorities.
++ * </p>
++ *
++ * @return {@code true} if a task was executed, {@code false} otherwise
++ * @throws IllegalStateException If the current thread is not allowed to execute a task
++ */
++ public boolean executeTask() throws IllegalStateException;
++
++ /**
++ * Prevent further additions to this executor. Attempts to add after this call has completed (potentially during) will
++ * result in {@link IllegalStateException} being thrown.
++ * <p>
++ * This operation is atomic with respect to other shutdown calls
++ * </p>
++ * <p>
++ * After this call has completed, regardless of return value, this executor will be shutdown.
++ * </p>
++ *
++ * @return {@code true} if the executor was shutdown, {@code false} if it has shut down already
++ * @see #isShutdown()
++ */
++ public boolean shutdown();
++
++ /**
++ * Returns whether this executor has shut down. Effectively, returns whether new tasks will be rejected.
++ * This method does not indicate whether all the tasks scheduled have been executed.
++ * @return Returns whether this executor has shut down.
++ */
++ public boolean isShutdown();
++
++ /**
++ * Queues or executes a task at {@link Priority#NORMAL} priority.
++ * @param task The task to run.
++ *
++ * @throws IllegalStateException If this executor has shutdown.
++ * @throws NullPointerException If the task is null
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask queueTask(final Runnable task);
++
++ /**
++ * Queues or executes a task.
++ *
++ * @param task The task to run.
++ * @param priority The priority for the task.
++ *
++ * @throws IllegalStateException If this executor has shutdown.
++ * @throws NullPointerException If the task is null
++ * @throws IllegalArgumentException If the priority is invalid.
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority);
++
++ /**
++ * Queues or executes a task.
++ *
++ * @param task The task to run.
++ * @param priority The priority for the task.
++ * @param subOrder The task's suborder.
++ *
++ * @throws IllegalStateException If this executor has shutdown.
++ * @throws NullPointerException If the task is null
++ * @throws IllegalArgumentException If the priority is invalid.
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder);
++
++ /**
++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++ * @param task The task to run.
++ *
++ * @throws NullPointerException If the task is null
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask createTask(final Runnable task);
++
++ /**
++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++ *
++ * @param task The task to run.
++ * @param priority The priority for the task.
++ *
++ * @throws NullPointerException If the task is null
++ * @throws IllegalArgumentException If the priority is invalid.
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask createTask(final Runnable task, final Priority priority);
++
++ /**
++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++ *
++ * @param task The task to run.
++ * @param priority The priority for the task.
++ * @param subOrder The task's suborder.
++ *
++ * @throws NullPointerException If the task is null
++ * @throws IllegalArgumentException If the priority is invalid.
++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++ * associated with the parameter
++ */
++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder);
++
++ public static interface PrioritisedTask extends Cancellable {
++
++ /**
++ * Returns the executor associated with this task.
++ * @return The executor associated with this task.
++ */
++ public PrioritisedExecutor getExecutor();
++
++ /**
++ * Causes a lazily queued task to become queued or executed
++ *
++ * @throws IllegalStateException If the backing executor has shutdown
++ * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
++ */
++ public boolean queue();
++
++ /**
++ * Returns whether this task has been queued and is not completing.
++ * @return {@code true} If the task has been queued, {@code false} if the task has not been queued or is marked
++ * as completing.
++ */
++ public boolean isQueued();
++
++ /**
++ * Forces this task to be marked as completed.
++ *
++ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed
++ * or is being completed.
++ */
++ @Override
++ public boolean cancel();
++
++ /**
++ * Executes this task. This will also mark the task as completing.
++ * <p>
++ * Exceptions thrown from the runnable will be rethrown.
++ * </p>
++ *
++ * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
++ */
++ public boolean execute();
++
++ /**
++ * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
++ * if this task is completing or has completed.
++ */
++ public Priority getPriority();
++
++ /**
++ * Attempts to set this task's priority level to the level specified.
++ *
++ * @param priority Specified priority level.
++ *
++ * @throws IllegalArgumentException If the priority is invalid
++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++ * this task was scheduled on was shutdown, or if the priority was already at the specified level.
++ */
++ public boolean setPriority(final Priority priority);
++
++ /**
++ * Attempts to raise the priority to the priority level specified.
++ *
++ * @param priority Priority specified
++ *
++ * @throws IllegalArgumentException If the priority is invalid
++ * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the
++ * specified level or was already at the specified level or higher.
++ */
++ public boolean raisePriority(final Priority priority);
++
++ /**
++ * Attempts to lower the priority to the priority level specified.
++ *
++ * @param priority Priority specified
++ *
++ * @throws IllegalArgumentException If the priority is invalid
++ * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the
++ * specified level or was already at the specified level or lower.
++ */
++ public boolean lowerPriority(final Priority priority);
++
++ /**
++ * Returns the suborder id associated with this task.
++ * @return The suborder id associated with this task.
++ */
++ public long getSubOrder();
++
++ /**
++ * Sets the suborder id associated with this task. Ths function has no effect when this task
++ * is completing or is completed.
++ *
++ * @param subOrder Specified new sub order.
++ *
++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++ * this task was scheduled on was shutdown, or if the current suborder is the same as the new sub order.
++ */
++ public boolean setSubOrder(final long subOrder);
++
++ /**
++ * Attempts to raise the suborder to the suborder specified.
++ *
++ * @param subOrder Specified new sub order.
++ *
++ * @return {@code false} if the current task is completing, {@code true} if the suborder was raised to the
++ * specified suborder or was already at the specified suborder or higher.
++ */
++ public boolean raiseSubOrder(final long subOrder);
++
++ /**
++ * Attempts to lower the suborder to the suborder specified.
++ *
++ * @param subOrder Specified new sub order.
++ *
++ * @return {@code false} if the current task is completing, {@code true} if the suborder was lowered to the
++ * specified suborder or was already at the specified suborder or lower.
++ */
++ public boolean lowerSubOrder(final long subOrder);
++
++ /**
++ * Sets the priority and suborder id associated with this task. Ths function has no effect when this task
++ * is completing or is completed.
++ *
++ * @param priority Priority specified
++ * @param subOrder Specified new sub order.
++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++ * this task was scheduled on was shutdown, or if the current priority and suborder are the same as
++ * the parameters.
++ */
++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..edb8c6611bdc9aced2714b963e00bbb7829603d2
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java
+@@ -0,0 +1,454 @@
++package ca.spottedleaf.concurrentutil.executor.queue;
++
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import java.lang.invoke.VarHandle;
++import java.util.Comparator;
++import java.util.Map;
++import java.util.concurrent.ConcurrentSkipListMap;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicLong;
++
++public final class PrioritisedTaskQueue implements PrioritisedExecutor {
++
++ /**
++ * Required for tie-breaking in the queue
++ */
++ private final AtomicLong taskIdGenerator = new AtomicLong();
++ private final AtomicLong scheduledTasks = new AtomicLong();
++ private final AtomicLong executedTasks = new AtomicLong();
++ private final AtomicLong subOrderGenerator = new AtomicLong();
++ private final AtomicBoolean shutdown = new AtomicBoolean();
++ private final ConcurrentSkipListMap<PrioritisedQueuedTask.Holder, Boolean> tasks = new ConcurrentSkipListMap<>(PrioritisedQueuedTask.COMPARATOR);
++
++ @Override
++ public long getTotalTasksScheduled() {
++ return this.scheduledTasks.get();
++ }
++
++ @Override
++ public long getTotalTasksExecuted() {
++ return this.executedTasks.get();
++ }
++
++ @Override
++ public long generateNextSubOrder() {
++ return this.subOrderGenerator.getAndIncrement();
++ }
++
++ @Override
++ public boolean shutdown() {
++ return !this.shutdown.getAndSet(true);
++ }
++
++ @Override
++ public boolean isShutdown() {
++ return this.shutdown.get();
++ }
++
++ public PrioritisedTask peekFirst() {
++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++ return firstEntry == null ? null : firstEntry.getKey().task;
++ }
++
++ public Priority getHighestPriority() {
++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++ return firstEntry == null ? null : Priority.getPriority(firstEntry.getKey().priority);
++ }
++
++ public boolean hasNoScheduledTasks() {
++ final long executedTasks = this.executedTasks.get();
++ final long scheduledTasks = this.scheduledTasks.get();
++
++ return executedTasks == scheduledTasks;
++ }
++
++ public PrioritySubOrderPair getHighestPrioritySubOrder() {
++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++ if (firstEntry == null) {
++ return null;
++ }
++
++ final PrioritisedQueuedTask.Holder holder = firstEntry.getKey();
++
++ return new PrioritySubOrderPair(Priority.getPriority(holder.priority), holder.subOrder);
++ }
++
++ public Runnable pollTask() {
++ for (;;) {
++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry();
++ if (firstEntry != null) {
++ final PrioritisedQueuedTask.Holder task = firstEntry.getKey();
++ task.markRemoved();
++ if (!task.task.cancel()) {
++ continue;
++ }
++ return task.task.execute;
++ }
++
++ return null;
++ }
++ }
++
++ @Override
++ public boolean executeTask() {
++ for (;;) {
++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry();
++ if (firstEntry != null) {
++ final PrioritisedQueuedTask.Holder task = firstEntry.getKey();
++ task.markRemoved();
++ if (!task.task.execute()) {
++ continue;
++ }
++ return true;
++ }
++
++ return false;
++ }
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task) {
++ return this.createTask(task, Priority.NORMAL, this.generateNextSubOrder());
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++ return this.createTask(task, priority, this.generateNextSubOrder());
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++ return new PrioritisedQueuedTask(task, priority, subOrder);
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task) {
++ return this.queueTask(task, Priority.NORMAL, this.generateNextSubOrder());
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++ return this.queueTask(task, priority, this.generateNextSubOrder());
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++ final PrioritisedQueuedTask ret = new PrioritisedQueuedTask(task, priority, subOrder);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ private final class PrioritisedQueuedTask implements PrioritisedExecutor.PrioritisedTask {
++ public static final Comparator<PrioritisedQueuedTask.Holder> COMPARATOR = (final PrioritisedQueuedTask.Holder t1, final PrioritisedQueuedTask.Holder t2) -> {
++ final int priorityCompare = t1.priority - t2.priority;
++ if (priorityCompare != 0) {
++ return priorityCompare;
++ }
++
++ final int subOrderCompare = Long.compare(t1.subOrder, t2.subOrder);
++ if (subOrderCompare != 0) {
++ return subOrderCompare;
++ }
++
++ return Long.compare(t1.id, t2.id);
++ };
++
++ private static final class Holder {
++ private final PrioritisedQueuedTask task;
++ private final int priority;
++ private final long subOrder;
++ private final long id;
++
++ private volatile boolean removed;
++ private static final VarHandle REMOVED_HANDLE = ConcurrentUtil.getVarHandle(Holder.class, "removed", boolean.class);
++
++ private Holder(final PrioritisedQueuedTask task, final int priority, final long subOrder,
++ final long id) {
++ this.task = task;
++ this.priority = priority;
++ this.subOrder = subOrder;
++ this.id = id;
++ }
++
++ /**
++ * Returns true if marked as removed
++ */
++ public boolean markRemoved() {
++ return !(boolean)REMOVED_HANDLE.getAndSet((Holder)this, (boolean)true);
++ }
++ }
++
++ private final long id;
++ private final Runnable execute;
++
++ private Priority priority;
++ private long subOrder;
++ private Holder holder;
++
++ public PrioritisedQueuedTask(final Runnable execute, final Priority priority, final long subOrder) {
++ if (!Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++
++ this.execute = execute;
++ this.priority = priority;
++ this.subOrder = subOrder;
++ this.id = PrioritisedTaskQueue.this.taskIdGenerator.getAndIncrement();
++ }
++
++ @Override
++ public PrioritisedExecutor getExecutor() {
++ return PrioritisedTaskQueue.this;
++ }
++
++ @Override
++ public boolean queue() {
++ synchronized (this) {
++ if (this.holder != null || this.priority == Priority.COMPLETING) {
++ return false;
++ }
++
++ if (PrioritisedTaskQueue.this.isShutdown()) {
++ throw new IllegalStateException("Queue is shutdown");
++ }
++
++ final Holder holder = new Holder(this, this.priority.priority, this.subOrder, this.id);
++ this.holder = holder;
++
++ PrioritisedTaskQueue.this.scheduledTasks.getAndIncrement();
++ PrioritisedTaskQueue.this.tasks.put(holder, Boolean.TRUE);
++ }
++
++ if (PrioritisedTaskQueue.this.isShutdown()) {
++ this.cancel();
++ throw new IllegalStateException("Queue is shutdown");
++ }
++
++
++ return true;
++ }
++
++ @Override
++ public boolean isQueued() {
++ synchronized (this) {
++ return this.holder != null && this.priority != Priority.COMPLETING;
++ }
++ }
++
++ @Override
++ public boolean cancel() {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING) {
++ return false;
++ }
++
++ this.priority = Priority.COMPLETING;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ PrioritisedTaskQueue.this.executedTasks.getAndIncrement();
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean execute() {
++ final boolean increaseExecuted;
++
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING) {
++ return false;
++ }
++
++ this.priority = Priority.COMPLETING;
++
++ if (increaseExecuted = (this.holder != null)) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ }
++ }
++
++ try {
++ this.execute.run();
++ return true;
++ } finally {
++ if (increaseExecuted) {
++ PrioritisedTaskQueue.this.executedTasks.getAndIncrement();
++ }
++ }
++ }
++
++ @Override
++ public Priority getPriority() {
++ synchronized (this) {
++ return this.priority;
++ }
++ }
++
++ @Override
++ public boolean setPriority(final Priority priority) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.priority == priority) {
++ return false;
++ }
++
++ this.priority = priority;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean raisePriority(final Priority priority) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.priority.isHigherOrEqualPriority(priority)) {
++ return false;
++ }
++
++ this.priority = priority;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean lowerPriority(Priority priority) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.priority.isLowerOrEqualPriority(priority)) {
++ return false;
++ }
++
++ this.priority = priority;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public long getSubOrder() {
++ synchronized (this) {
++ return this.subOrder;
++ }
++ }
++
++ @Override
++ public boolean setSubOrder(final long subOrder) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.subOrder == subOrder) {
++ return false;
++ }
++
++ this.subOrder = subOrder;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean raiseSubOrder(long subOrder) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.subOrder >= subOrder) {
++ return false;
++ }
++
++ this.subOrder = subOrder;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean lowerSubOrder(final long subOrder) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || this.subOrder <= subOrder) {
++ return false;
++ }
++
++ this.subOrder = subOrder;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++ synchronized (this) {
++ if (this.priority == Priority.COMPLETING || (this.priority == priority && this.subOrder == subOrder)) {
++ return false;
++ }
++
++ this.priority = priority;
++ this.subOrder = subOrder;
++
++ if (this.holder != null) {
++ if (this.holder.markRemoved()) {
++ PrioritisedTaskQueue.this.tasks.remove(this.holder);
++ }
++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++ }
++
++ return true;
++ }
++ }
++ }
++
++ public static record PrioritySubOrderPair(Priority priority, long subOrder) {}
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f5367a13aaa02f0f929813c00a67e6ac7c8652cb
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java
+@@ -0,0 +1,402 @@
++package ca.spottedleaf.concurrentutil.executor.thread;
++
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.locks.LockSupport;
++
++/**
++ * Thread which will continuously drain from a specified queue.
++ * <p>
++ * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread
++ * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour
++ * of task scheduling, use the methods provided on this class to schedule tasks.
++ * </p>
++ */
++public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedQueueExecutorThread.class);
++
++ protected final PrioritisedExecutor queue;
++
++ protected volatile boolean threadShutdown;
++
++ protected volatile boolean threadParked;
++ protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
++
++ protected volatile boolean halted;
++
++ protected final long spinWaitTime;
++
++ protected static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
++
++ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) {
++ this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms
++ }
++
++ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns
++ this.queue = queue;
++ this.spinWaitTime = spinWaitTime;
++ }
++
++ @Override
++ public final void run() {
++ try {
++ this.begin();
++ this.doRun();
++ } finally {
++ this.die();
++ }
++ }
++
++ public final void doRun() {
++ final long spinWaitTime = this.spinWaitTime;
++
++ main_loop:
++ for (;;) {
++ this.pollTasks();
++
++ // spinwait
++
++ final long start = System.nanoTime();
++
++ for (;;) {
++ // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
++ Thread.interrupted();
++ Thread.yield();
++ LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us
++
++ if (this.pollTasks()) {
++ // restart loop, found tasks
++ continue main_loop;
++ }
++
++ if (this.handleClose()) {
++ return; // we're done
++ }
++
++ if ((System.nanoTime() - start) >= spinWaitTime) {
++ break;
++ }
++ }
++
++ if (this.handleClose()) {
++ return;
++ }
++
++ this.setThreadParkedVolatile(true);
++
++ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
++ // (i.e. it will not notify us)
++ if (this.pollTasks()) {
++ this.setThreadParkedVolatile(false);
++ continue;
++ }
++
++ if (this.handleClose()) {
++ return;
++ }
++
++ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
++ // LockSupport.park() can fail for any reason
++ while (this.getThreadParkedVolatile()) {
++ Thread.interrupted();
++ LockSupport.park("Waiting on tasks");
++ }
++ }
++ }
++
++ protected void begin() {}
++
++ protected void die() {}
++
++ /**
++ * Attempts to poll as many tasks as possible, returning when finished.
++ * @return Whether any tasks were executed.
++ */
++ protected boolean pollTasks() {
++ boolean ret = false;
++
++ for (;;) {
++ if (this.halted) {
++ break;
++ }
++ try {
++ if (!this.queue.executeTask()) {
++ break;
++ }
++ ret = true;
++ } catch (final Throwable throwable) {
++ LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable);
++ }
++ }
++
++ return ret;
++ }
++
++ protected boolean handleClose() {
++ if (this.threadShutdown) {
++ this.pollTasks(); // this ensures we've emptied the queue
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Notify this thread that a task has been added to its queue
++ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
++ */
++ public boolean notifyTasks() {
++ if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) {
++ LockSupport.unpark(this);
++ return true;
++ }
++ return false;
++ }
++
++ @Override
++ public long getTotalTasksExecuted() {
++ return this.queue.getTotalTasksExecuted();
++ }
++
++ @Override
++ public long getTotalTasksScheduled() {
++ return this.queue.getTotalTasksScheduled();
++ }
++
++ @Override
++ public long generateNextSubOrder() {
++ return this.queue.generateNextSubOrder();
++ }
++
++ @Override
++ public boolean shutdown() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean isShutdown() {
++ return false;
++ }
++
++ /**
++ * {@inheritDoc}
++ * @throws IllegalStateException Always
++ */
++ @Override
++ public boolean executeTask() throws IllegalStateException {
++ throw new IllegalStateException();
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task) {
++ final PrioritisedTask ret = this.createTask(task);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++ final PrioritisedTask ret = this.createTask(task, priority);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++ final PrioritisedTask ret = this.createTask(task, priority, subOrder);
++
++ ret.queue();
++
++ return ret;
++ }
++
++
++ @Override
++ public PrioritisedTask createTask(Runnable task) {
++ final PrioritisedTask queueTask = this.queue.createTask(task);
++
++ return new WrappedTask(queueTask);
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++ final PrioritisedTask queueTask = this.queue.createTask(task, priority);
++
++ return new WrappedTask(queueTask);
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++ final PrioritisedTask queueTask = this.queue.createTask(task, priority, subOrder);
++
++ return new WrappedTask(queueTask);
++ }
++
++ /**
++ * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true.
++ * <p>
++ * This function is MT-Safe.
++ * </p>
++ * @param wait If this call is to wait until this thread shuts down.
++ * @param killQueue Whether to shutdown this thread's queue
++ * @return whether this thread shut down the queue
++ * @see #halt(boolean)
++ */
++ public boolean close(final boolean wait, final boolean killQueue) {
++ final boolean ret = killQueue && this.queue.shutdown();
++ this.threadShutdown = true;
++
++ // force thread to respond to the shutdown
++ this.setThreadParkedVolatile(false);
++ LockSupport.unpark(this);
++
++ if (wait) {
++ boolean interrupted = false;
++ for (;;) {
++ if (this.isAlive()) {
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++ break;
++ }
++ try {
++ this.join();
++ } catch (final InterruptedException ex) {
++ interrupted = true;
++ }
++ }
++ }
++
++ return ret;
++ }
++
++
++ /**
++ * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}.
++ * <p>
++ * This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case
++ * the waiting thread may block indefinitely.
++ * </p>
++ * <p>
++ * This function is MT-Safe.
++ * </p>
++ * @param killQueue Whether to shutdown this thread's queue
++ * @see #close(boolean, boolean)
++ */
++ public void halt(final boolean killQueue) {
++ if (killQueue) {
++ this.queue.shutdown();
++ }
++ this.threadShutdown = true;
++ this.halted = true;
++
++ // force thread to respond to the shutdown
++ this.setThreadParkedVolatile(false);
++ LockSupport.unpark(this);
++ }
++
++ protected final boolean getThreadParkedVolatile() {
++ return (boolean)THREAD_PARKED_HANDLE.getVolatile(this);
++ }
++
++ protected final boolean exchangeThreadParkedVolatile(final boolean value) {
++ return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value);
++ }
++
++ protected final void setThreadParkedVolatile(final boolean value) {
++ THREAD_PARKED_HANDLE.setVolatile(this, value);
++ }
++
++ /**
++ * Required so that queue() can notify (unpark) this thread
++ */
++ private final class WrappedTask implements PrioritisedTask {
++ private final PrioritisedTask queueTask;
++
++ public WrappedTask(final PrioritisedTask queueTask) {
++ this.queueTask = queueTask;
++ }
++
++ @Override
++ public PrioritisedExecutor getExecutor() {
++ return PrioritisedQueueExecutorThread.this;
++ }
++
++ @Override
++ public boolean queue() {
++ final boolean ret = this.queueTask.queue();
++ if (ret) {
++ PrioritisedQueueExecutorThread.this.notifyTasks();
++ }
++ return ret;
++ }
++
++ @Override
++ public boolean isQueued() {
++ return this.queueTask.isQueued();
++ }
++
++ @Override
++ public boolean cancel() {
++ return this.queueTask.cancel();
++ }
++
++ @Override
++ public boolean execute() {
++ return this.queueTask.execute();
++ }
++
++ @Override
++ public Priority getPriority() {
++ return this.queueTask.getPriority();
++ }
++
++ @Override
++ public boolean setPriority(final Priority priority) {
++ return this.queueTask.setPriority(priority);
++ }
++
++ @Override
++ public boolean raisePriority(final Priority priority) {
++ return this.queueTask.raisePriority(priority);
++ }
++
++ @Override
++ public boolean lowerPriority(final Priority priority) {
++ return this.queueTask.lowerPriority(priority);
++ }
++
++ @Override
++ public long getSubOrder() {
++ return this.queueTask.getSubOrder();
++ }
++
++ @Override
++ public boolean setSubOrder(final long subOrder) {
++ return this.queueTask.setSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean raiseSubOrder(final long subOrder) {
++ return this.queueTask.raiseSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean lowerSubOrder(final long subOrder) {
++ return this.queueTask.lowerSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++ return this.queueTask.setPriorityAndSubOrder(priority, subOrder);
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cb9df914a9a6d0d3f58fa58d8c93f4f583416cd1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java
+@@ -0,0 +1,741 @@
++package ca.spottedleaf.concurrentutil.executor.thread;
++
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.function.Consumer;
++
++public final class PrioritisedThreadPool {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class);
++
++ private final Consumer<Thread> threadModifier;
++ private final COWArrayList<ExecutorGroup> executors = new COWArrayList<>(ExecutorGroup.class);
++ private final COWArrayList<PrioritisedThread> threads = new COWArrayList<>(PrioritisedThread.class);
++ private final COWArrayList<PrioritisedThread> aliveThreads = new COWArrayList<>(PrioritisedThread.class);
++
++ private static final Priority HIGH_PRIORITY_NOTIFY_THRESHOLD = Priority.HIGH;
++ private static final Priority QUEUE_SHUTDOWN_PRIORITY = Priority.HIGH;
++
++ private boolean shutdown;
++
++ public PrioritisedThreadPool(final Consumer<Thread> threadModifier) {
++ this.threadModifier = threadModifier;
++
++ if (threadModifier == null) {
++ throw new NullPointerException("Thread factory may not be null");
++ }
++ }
++
++ public Thread[] getAliveThreads() {
++ final PrioritisedThread[] threads = this.aliveThreads.getArray();
++
++ return Arrays.copyOf(threads, threads.length, Thread[].class);
++ }
++
++ public Thread[] getCoreThreads() {
++ final PrioritisedThread[] threads = this.threads.getArray();
++
++ return Arrays.copyOf(threads, threads.length, Thread[].class);
++ }
++
++ /**
++ * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
++ */
++ public void halt(final boolean shutdownQueues) {
++ synchronized (this) {
++ this.shutdown = true;
++ }
++
++ if (shutdownQueues) {
++ for (final ExecutorGroup group : this.executors.getArray()) {
++ for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) {
++ executor.shutdown();
++ }
++ }
++ }
++
++ for (final PrioritisedThread thread : this.threads.getArray()) {
++ thread.halt(false);
++ }
++ }
++
++ /**
++ * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++ * @param msToWait Maximum time to wait.
++ * @return {@code false} if the maximum time passed, {@code true} otherwise.
++ */
++ public boolean join(final long msToWait) {
++ try {
++ return this.join(msToWait, false);
++ } catch (final InterruptedException ex) {
++ throw new IllegalStateException(ex);
++ }
++ }
++
++ /**
++ * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++ * @param msToWait Maximum time to wait.
++ * @return {@code false} if the maximum time passed, {@code true} otherwise.
++ * @throws InterruptedException If this thread is interrupted.
++ */
++ public boolean joinInterruptable(final long msToWait) throws InterruptedException {
++ return this.join(msToWait, true);
++ }
++
++ protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
++ final long nsToWait = msToWait * (1000 * 1000);
++ final long start = System.nanoTime();
++ final long deadline = start + nsToWait;
++ boolean interrupted = false;
++ try {
++ for (final PrioritisedThread thread : this.aliveThreads.getArray()) {
++ for (;;) {
++ if (!thread.isAlive()) {
++ break;
++ }
++ final long current = System.nanoTime();
++ if (current >= deadline && msToWait > 0L) {
++ return false;
++ }
++
++ try {
++ thread.join(msToWait <= 0L ? 0L : Math.max(1L, (deadline - current) / (1000 * 1000)));
++ } catch (final InterruptedException ex) {
++ if (interruptable) {
++ throw ex;
++ }
++ interrupted = true;
++ }
++ }
++ }
++
++ return true;
++ } finally {
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++ }
++ }
++
++ /**
++ * Shuts down this thread pool, optionally waiting for all tasks to be executed.
++ * This function will invoke {@link PrioritisedExecutor#shutdown()} on all created executors on this
++ * thread pool.
++ * @param wait Whether to wait for tasks to be executed
++ */
++ public void shutdown(final boolean wait) {
++ synchronized (this) {
++ this.shutdown = true;
++ }
++
++ for (final ExecutorGroup group : this.executors.getArray()) {
++ for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) {
++ executor.shutdown();
++ }
++ }
++
++
++ for (final PrioritisedThread thread : this.threads.getArray()) {
++ // none of these can be true or else NPE
++ thread.close(false, false);
++ }
++
++ if (wait) {
++ this.join(0L);
++ }
++ }
++
++ private void die(final PrioritisedThread thread) {
++ this.aliveThreads.remove(thread);
++ }
++
++ public void adjustThreadCount(final int threads) {
++ synchronized (this) {
++ if (this.shutdown) {
++ return;
++ }
++
++ final PrioritisedThread[] currentThreads = this.threads.getArray();
++ if (threads == currentThreads.length) {
++ // no adjustment needed
++ return;
++ }
++
++ if (threads < currentThreads.length) {
++ // we need to trim threads
++ for (int i = 0, difference = currentThreads.length - threads; i < difference; ++i) {
++ final PrioritisedThread remove = currentThreads[currentThreads.length - i - 1];
++
++ remove.halt(false);
++ this.threads.remove(remove);
++ }
++ } else {
++ // we need to add threads
++ for (int i = 0, difference = threads - currentThreads.length; i < difference; ++i) {
++ final PrioritisedThread thread = new PrioritisedThread();
++
++ this.threadModifier.accept(thread);
++ this.aliveThreads.add(thread);
++ this.threads.add(thread);
++
++ thread.start();
++ }
++ }
++ }
++ }
++
++ private static int compareInsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority,
++ final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) {
++ final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal();
++ if (priorityCompare != 0) {
++ return priorityCompare;
++ }
++
++ final int parallelismCompare = src.currentParallelism - dst.currentParallelism;
++ if (parallelismCompare != 0) {
++ return parallelismCompare;
++ }
++
++ return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved);
++ }
++
++ private static int compareOutsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority,
++ final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) {
++ if (src.getGroup().division == dst.getGroup().division) {
++ // can only compare priorities inside the same division
++ final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal();
++ if (priorityCompare != 0) {
++ return priorityCompare;
++ }
++ }
++
++ final int parallelismCompare = src.getGroup().currentParallelism - dst.getGroup().currentParallelism;
++ if (parallelismCompare != 0) {
++ return parallelismCompare;
++ }
++
++ return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved);
++ }
++
++ private ExecutorGroup.ThreadPoolExecutor obtainQueue() {
++ final long time = System.nanoTime();
++ synchronized (this) {
++ ExecutorGroup.ThreadPoolExecutor ret = null;
++ Priority retPriority = null;
++
++ for (final ExecutorGroup executorGroup : this.executors.getArray()) {
++ ExecutorGroup.ThreadPoolExecutor highest = null;
++ Priority highestPriority = null;
++ for (final ExecutorGroup.ThreadPoolExecutor executor : executorGroup.executors.getArray()) {
++ final int maxParallelism = executor.maxParallelism;
++ if (maxParallelism > 0 && executor.currentParallelism >= maxParallelism) {
++ continue;
++ }
++
++ final Priority priority = executor.getTargetPriority();
++
++ if (priority == null) {
++ continue;
++ }
++
++ if (highestPriority == null || compareInsideGroup(highest, highestPriority, executor, priority) > 0) {
++ highest = executor;
++ highestPriority = priority;
++ }
++ }
++
++ if (highest == null) {
++ continue;
++ }
++
++ if (ret == null || compareOutsideGroup(ret, retPriority, highest, highestPriority) > 0) {
++ ret = highest;
++ retPriority = highestPriority;
++ }
++ }
++
++ if (ret != null) {
++ ret.lastRetrieved = time;
++ ++ret.currentParallelism;
++ ++ret.getGroup().currentParallelism;
++ return ret;
++ }
++
++ return ret;
++ }
++ }
++
++ private void returnQueue(final ExecutorGroup.ThreadPoolExecutor executor) {
++ synchronized (this) {
++ --executor.currentParallelism;
++ --executor.getGroup().currentParallelism;
++ }
++
++ if (executor.isShutdown() && executor.queue.hasNoScheduledTasks()) {
++ executor.getGroup().executors.remove(executor);
++ }
++ }
++
++ private void notifyAllThreads() {
++ for (final PrioritisedThread thread : this.threads.getArray()) {
++ thread.notifyTasks();
++ }
++ }
++
++ public ExecutorGroup createExecutorGroup(final int division, final int flags) {
++ synchronized (this) {
++ if (this.shutdown) {
++ throw new IllegalStateException("Queue is shutdown: " + this.toString());
++ }
++
++ final ExecutorGroup ret = new ExecutorGroup(division, flags);
++
++ this.executors.add(ret);
++
++ return ret;
++ }
++ }
++
++ private final class PrioritisedThread extends PrioritisedQueueExecutorThread {
++
++ private final AtomicBoolean alertedHighPriority = new AtomicBoolean();
++
++ public PrioritisedThread() {
++ super(null);
++ }
++
++ public boolean alertHighPriorityExecutor() {
++ if (!this.notifyTasks()) {
++ if (!this.alertedHighPriority.get()) {
++ this.alertedHighPriority.set(true);
++ }
++ return false;
++ }
++
++ return true;
++ }
++
++ private boolean isAlertedHighPriority() {
++ return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
++ }
++
++ @Override
++ protected void die() {
++ PrioritisedThreadPool.this.die(this);
++ }
++
++ @Override
++ protected boolean pollTasks() {
++ boolean ret = false;
++
++ for (;;) {
++ if (this.halted) {
++ break;
++ }
++
++ final ExecutorGroup.ThreadPoolExecutor executor = PrioritisedThreadPool.this.obtainQueue();
++ if (executor == null) {
++ break;
++ }
++ final long deadline = System.nanoTime() + executor.queueMaxHoldTime;
++ do {
++ try {
++ if (this.halted || executor.halt) {
++ break;
++ }
++ if (!executor.executeTask()) {
++ // no more tasks, try next queue
++ break;
++ }
++ ret = true;
++ } catch (final Throwable throwable) {
++ LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + executor.toString() + "'", throwable);
++ }
++ } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
++
++ PrioritisedThreadPool.this.returnQueue(executor);
++ }
++
++
++ return ret;
++ }
++ }
++
++ public final class ExecutorGroup {
++
++ private final AtomicLong subOrderGenerator = new AtomicLong();
++ private final COWArrayList<ThreadPoolExecutor> executors = new COWArrayList<>(ThreadPoolExecutor.class);
++
++ private final int division;
++ private int currentParallelism;
++
++ private ExecutorGroup(final int division, final int flags) {
++ this.division = division;
++ }
++
++ public ThreadPoolExecutor[] getAllExecutors() {
++ return this.executors.getArray().clone();
++ }
++
++ private PrioritisedThreadPool getThreadPool() {
++ return PrioritisedThreadPool.this;
++ }
++
++ public ThreadPoolExecutor createExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) {
++ synchronized (PrioritisedThreadPool.this) {
++ if (PrioritisedThreadPool.this.shutdown) {
++ throw new IllegalStateException("Queue is shutdown: " + PrioritisedThreadPool.this.toString());
++ }
++
++ final ThreadPoolExecutor ret = new ThreadPoolExecutor(maxParallelism, queueMaxHoldTime, flags);
++
++ this.executors.add(ret);
++
++ return ret;
++ }
++ }
++
++ public final class ThreadPoolExecutor implements PrioritisedExecutor {
++
++ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue();
++
++ private volatile int maxParallelism;
++ private final long queueMaxHoldTime;
++ private volatile int currentParallelism;
++ private volatile boolean halt;
++ private long lastRetrieved = System.nanoTime();
++
++ private ThreadPoolExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) {
++ this.maxParallelism = maxParallelism;
++ this.queueMaxHoldTime = queueMaxHoldTime;
++ }
++
++ private ExecutorGroup getGroup() {
++ return ExecutorGroup.this;
++ }
++
++ private boolean canNotify() {
++ if (this.halt) {
++ return false;
++ }
++
++ final int max = this.maxParallelism;
++ return max < 0 || this.currentParallelism < max;
++ }
++
++ private void notifyHighPriority() {
++ if (!this.canNotify()) {
++ return;
++ }
++ for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) {
++ if (thread.alertHighPriorityExecutor()) {
++ return;
++ }
++ }
++ }
++
++ private void notifyScheduled() {
++ if (!this.canNotify()) {
++ return;
++ }
++ for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) {
++ if (thread.notifyTasks()) {
++ return;
++ }
++ }
++ }
++
++ /**
++ * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
++ */
++ public void halt() {
++ this.halt = true;
++
++ ExecutorGroup.this.executors.remove(this);
++ }
++
++ /**
++ * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
++ * this queue is not halted and not shutdown.
++ */
++ public boolean isActive() {
++ if (this.halt) {
++ return this.currentParallelism > 0;
++ } else {
++ if (!this.isShutdown()) {
++ return true;
++ }
++
++ return !this.queue.hasNoScheduledTasks();
++ }
++ }
++
++ @Override
++ public boolean shutdown() {
++ if (!this.queue.shutdown()) {
++ return false;
++ }
++
++ if (this.queue.hasNoScheduledTasks()) {
++ ExecutorGroup.this.executors.remove(this);
++ }
++
++ return true;
++ }
++
++ @Override
++ public boolean isShutdown() {
++ return this.queue.isShutdown();
++ }
++
++ public void setMaxParallelism(final int maxParallelism) {
++ this.maxParallelism = maxParallelism;
++ // assume that we could have increased the parallelism
++ if (this.getTargetPriority() != null) {
++ ExecutorGroup.this.getThreadPool().notifyAllThreads();
++ }
++ }
++
++ Priority getTargetPriority() {
++ final Priority ret = this.queue.getHighestPriority();
++ if (!this.isShutdown()) {
++ return ret;
++ }
++
++ return ret == null ? QUEUE_SHUTDOWN_PRIORITY : Priority.max(ret, QUEUE_SHUTDOWN_PRIORITY);
++ }
++
++ @Override
++ public long getTotalTasksScheduled() {
++ return this.queue.getTotalTasksScheduled();
++ }
++
++ @Override
++ public long getTotalTasksExecuted() {
++ return this.queue.getTotalTasksExecuted();
++ }
++
++ @Override
++ public long generateNextSubOrder() {
++ return ExecutorGroup.this.subOrderGenerator.getAndIncrement();
++ }
++
++ @Override
++ public boolean executeTask() {
++ return this.queue.executeTask();
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task) {
++ final PrioritisedTask ret = this.createTask(task);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++ final PrioritisedTask ret = this.createTask(task, priority);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ @Override
++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++ final PrioritisedTask ret = this.createTask(task, priority, subOrder);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task) {
++ return this.createTask(task, Priority.NORMAL);
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++ return this.createTask(task, priority, this.generateNextSubOrder());
++ }
++
++ @Override
++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++ return new WrappedTask(this.queue.createTask(task, priority, subOrder));
++ }
++
++ private final class WrappedTask implements PrioritisedTask {
++
++ private final PrioritisedTask wrapped;
++
++ private WrappedTask(final PrioritisedTask wrapped) {
++ this.wrapped = wrapped;
++ }
++
++ @Override
++ public PrioritisedExecutor getExecutor() {
++ return ThreadPoolExecutor.this;
++ }
++
++ @Override
++ public boolean queue() {
++ if (this.wrapped.queue()) {
++ final Priority priority = this.getPriority();
++ if (priority != Priority.COMPLETING) {
++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++ ThreadPoolExecutor.this.notifyHighPriority();
++ } else {
++ ThreadPoolExecutor.this.notifyScheduled();
++ }
++ }
++ return true;
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean isQueued() {
++ return this.wrapped.isQueued();
++ }
++
++ @Override
++ public boolean cancel() {
++ return this.wrapped.cancel();
++ }
++
++ @Override
++ public boolean execute() {
++ return this.wrapped.execute();
++ }
++
++ @Override
++ public Priority getPriority() {
++ return this.wrapped.getPriority();
++ }
++
++ @Override
++ public boolean setPriority(final Priority priority) {
++ if (this.wrapped.setPriority(priority)) {
++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++ ThreadPoolExecutor.this.notifyHighPriority();
++ }
++ return true;
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean raisePriority(final Priority priority) {
++ if (this.wrapped.raisePriority(priority)) {
++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++ ThreadPoolExecutor.this.notifyHighPriority();
++ }
++ return true;
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean lowerPriority(final Priority priority) {
++ return this.wrapped.lowerPriority(priority);
++ }
++
++ @Override
++ public long getSubOrder() {
++ return this.wrapped.getSubOrder();
++ }
++
++ @Override
++ public boolean setSubOrder(final long subOrder) {
++ return this.wrapped.setSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean raiseSubOrder(final long subOrder) {
++ return this.wrapped.raiseSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean lowerSubOrder(final long subOrder) {
++ return this.wrapped.lowerSubOrder(subOrder);
++ }
++
++ @Override
++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder)) {
++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++ ThreadPoolExecutor.this.notifyHighPriority();
++ }
++ return true;
++ }
++
++ return false;
++ }
++ }
++ }
++ }
++
++ private static final class COWArrayList<E> {
++
++ private volatile E[] array;
++
++ public COWArrayList(final Class<E> clazz) {
++ this.array = (E[])Array.newInstance(clazz, 0);
++ }
++
++ public E[] getArray() {
++ return this.array;
++ }
++
++ public void add(final E element) {
++ synchronized (this) {
++ final E[] array = this.array;
++
++ final E[] copy = Arrays.copyOf(array, array.length + 1);
++ copy[array.length] = element;
++
++ this.array = copy;
++ }
++ }
++
++ public boolean remove(final E element) {
++ synchronized (this) {
++ final E[] array = this.array;
++ int index = -1;
++ for (int i = 0, len = array.length; i < len; ++i) {
++ if (array[i] == element) {
++ index = i;
++ break;
++ }
++ }
++
++ if (index == -1) {
++ return false;
++ }
++
++ final E[] copy = (E[])Array.newInstance(array.getClass().getComponentType(), array.length - 1);
++
++ System.arraycopy(array, 0, copy, 0, index);
++ System.arraycopy(array, index + 1, copy, index, (array.length - 1) - index);
++
++ this.array = copy;
++ }
++
++ return true;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..94bfd7c56ffcea7d6491e94a7804bc3bd60fe9c3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
+@@ -0,0 +1,8 @@
++package ca.spottedleaf.concurrentutil.function;
++
++@FunctionalInterface
++public interface BiLong1Function<T, R> {
++
++ public R apply(final long t1, final T t2);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8e7eef07960a18d0593688eba55adfa1c85efadf
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
+@@ -0,0 +1,8 @@
++package ca.spottedleaf.concurrentutil.function;
++
++@FunctionalInterface
++public interface BiLongObjectConsumer<V> {
++
++ public void accept(final long key, final V value);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7ffe4379b06c03c56abbcbdee3bb720894a10702
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
+@@ -0,0 +1,350 @@
++package ca.spottedleaf.concurrentutil.lock;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.concurrentutil.util.IntPairUtil;
++import java.util.Objects;
++import java.util.concurrent.locks.LockSupport;
++
++public final class ReentrantAreaLock {
++
++ public final int coordinateShift;
++
++ // aggressive load factor to reduce contention
++ private final ConcurrentLong2ReferenceChainedHashTable<Node> nodes = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(128, 0.2f);
++
++ public ReentrantAreaLock(final int coordinateShift) {
++ this.coordinateShift = coordinateShift;
++ }
++
++ public boolean isHeldByCurrentThread(final int x, final int z) {
++ final Thread currThread = Thread.currentThread();
++ final int shift = this.coordinateShift;
++ final int sectionX = x >> shift;
++ final int sectionZ = z >> shift;
++
++ final long coordinate = IntPairUtil.key(sectionX, sectionZ);
++ final Node node = this.nodes.get(coordinate);
++
++ return node != null && node.thread == currThread;
++ }
++
++ public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) {
++ return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ }
++
++ public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) {
++ if (fromX > toX || fromZ > toZ) {
++ throw new IllegalArgumentException();
++ }
++
++ final Thread currThread = Thread.currentThread();
++ final int shift = this.coordinateShift;
++ final int fromSectionX = fromX >> shift;
++ final int fromSectionZ = fromZ >> shift;
++ final int toSectionX = toX >> shift;
++ final int toSectionZ = toZ >> shift;
++
++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
++ final long coordinate = IntPairUtil.key(currX, currZ);
++
++ final Node node = this.nodes.get(coordinate);
++
++ if (node == null || node.thread != currThread) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ public Node tryLock(final int x, final int z) {
++ return this.tryLock(x, z, x, z);
++ }
++
++ public Node tryLock(final int centerX, final int centerZ, final int radius) {
++ return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ }
++
++ public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) {
++ if (fromX > toX || fromZ > toZ) {
++ throw new IllegalArgumentException();
++ }
++
++ final Thread currThread = Thread.currentThread();
++ final int shift = this.coordinateShift;
++ final int fromSectionX = fromX >> shift;
++ final int fromSectionZ = fromZ >> shift;
++ final int toSectionX = toX >> shift;
++ final int toSectionZ = toZ >> shift;
++
++ final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
++ int areaAffectedLen = 0;
++
++ final Node ret = new Node(this, areaAffected, currThread);
++
++ boolean failed = false;
++
++ // try to fast acquire area
++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
++ final long coordinate = IntPairUtil.key(currX, currZ);
++
++ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++
++ if (prev == null) {
++ areaAffected[areaAffectedLen++] = coordinate;
++ continue;
++ }
++
++ if (prev.thread != currThread) {
++ failed = true;
++ break;
++ }
++ }
++ }
++
++ if (!failed) {
++ return ret;
++ }
++
++ // failed, undo logic
++ if (areaAffectedLen != 0) {
++ for (int i = 0; i < areaAffectedLen; ++i) {
++ final long key = areaAffected[i];
++
++ if (this.nodes.remove(key) != ret) {
++ throw new IllegalStateException();
++ }
++ }
++
++ areaAffectedLen = 0;
++
++ // since we inserted, we need to drain waiters
++ Thread unpark;
++ while ((unpark = ret.pollOrBlockAdds()) != null) {
++ LockSupport.unpark(unpark);
++ }
++ }
++
++ return null;
++ }
++
++ public Node lock(final int x, final int z) {
++ final Thread currThread = Thread.currentThread();
++ final int shift = this.coordinateShift;
++ final int sectionX = x >> shift;
++ final int sectionZ = z >> shift;
++
++ final long coordinate = IntPairUtil.key(sectionX, sectionZ);
++ final long[] areaAffected = new long[1];
++ areaAffected[0] = coordinate;
++
++ final Node ret = new Node(this, areaAffected, currThread);
++
++ for (long failures = 0L;;) {
++ final Node park;
++
++ // try to fast acquire area
++ {
++ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++
++ if (prev == null) {
++ ret.areaAffectedLen = 1;
++ return ret;
++ } else if (prev.thread != currThread) {
++ park = prev;
++ } else {
++ // only one node we would want to acquire, and it's owned by this thread already
++ // areaAffectedLen = 0 already
++ return ret;
++ }
++ }
++
++ ++failures;
++
++ if (failures > 128L && park.add(currThread)) {
++ LockSupport.park();
++ } else {
++ // high contention, spin wait
++ if (failures < 128L) {
++ for (long i = 0; i < failures; ++i) {
++ Thread.onSpinWait();
++ }
++ failures = failures << 1;
++ } else if (failures < 1_200L) {
++ LockSupport.parkNanos(1_000L);
++ failures = failures + 1L;
++ } else { // scale 0.1ms (100us) per failure
++ Thread.yield();
++ LockSupport.parkNanos(100_000L * failures);
++ failures = failures + 1L;
++ }
++ }
++ }
++ }
++
++ public Node lock(final int centerX, final int centerZ, final int radius) {
++ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ }
++
++ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) {
++ if (fromX > toX || fromZ > toZ) {
++ throw new IllegalArgumentException();
++ }
++
++ final Thread currThread = Thread.currentThread();
++ final int shift = this.coordinateShift;
++ final int fromSectionX = fromX >> shift;
++ final int fromSectionZ = fromZ >> shift;
++ final int toSectionX = toX >> shift;
++ final int toSectionZ = toZ >> shift;
++
++ if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) {
++ return this.lock(fromX, fromZ);
++ }
++
++ final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
++ int areaAffectedLen = 0;
++
++ final Node ret = new Node(this, areaAffected, currThread);
++
++ for (long failures = 0L;;) {
++ Node park = null;
++ boolean addedToArea = false;
++ boolean alreadyOwned = false;
++ boolean allOwned = true;
++
++ // try to fast acquire area
++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
++ final long coordinate = IntPairUtil.key(currX, currZ);
++
++ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++
++ if (prev == null) {
++ addedToArea = true;
++ allOwned = false;
++ areaAffected[areaAffectedLen++] = coordinate;
++ continue;
++ }
++
++ if (prev.thread != currThread) {
++ park = prev;
++ alreadyOwned = true;
++ break;
++ }
++ }
++ }
++
++ // check for failure
++ if ((park != null && addedToArea) || (park == null && alreadyOwned && !allOwned)) {
++ // failure to acquire: added and we need to block, or improper lock usage
++ for (int i = 0; i < areaAffectedLen; ++i) {
++ final long key = areaAffected[i];
++
++ if (this.nodes.remove(key) != ret) {
++ throw new IllegalStateException();
++ }
++ }
++
++ areaAffectedLen = 0;
++
++ // since we inserted, we need to drain waiters
++ Thread unpark;
++ while ((unpark = ret.pollOrBlockAdds()) != null) {
++ LockSupport.unpark(unpark);
++ }
++ }
++
++ if (park == null) {
++ if (alreadyOwned && !allOwned) {
++ throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas");
++ }
++ ret.areaAffectedLen = areaAffectedLen;
++ return ret;
++ }
++
++ // failed
++
++ ++failures;
++
++ if (failures > 128L && park.add(currThread)) {
++ LockSupport.park(park);
++ } else {
++ // high contention, spin wait
++ if (failures < 128L) {
++ for (long i = 0; i < failures; ++i) {
++ Thread.onSpinWait();
++ }
++ failures = failures << 1;
++ } else if (failures < 1_200L) {
++ LockSupport.parkNanos(1_000L);
++ failures = failures + 1L;
++ } else { // scale 0.1ms (100us) per failure
++ Thread.yield();
++ LockSupport.parkNanos(100_000L * failures);
++ failures = failures + 1L;
++ }
++ }
++
++ if (addedToArea) {
++ // try again, so we need to allow adds so that other threads can properly block on us
++ ret.allowAdds();
++ }
++ }
++ }
++
++ public void unlock(final Node node) {
++ if (node.lock != this) {
++ throw new IllegalStateException("Unlock target lock mismatch");
++ }
++
++ final long[] areaAffected = node.areaAffected;
++ final int areaAffectedLen = node.areaAffectedLen;
++
++ if (areaAffectedLen == 0) {
++ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters
++ return;
++ }
++
++ Objects.checkFromToIndex(0, areaAffectedLen, areaAffected.length);
++
++ // remove from node map; allowing other threads to lock
++ for (int i = 0; i < areaAffectedLen; ++i) {
++ final long coordinate = areaAffected[i];
++ if (this.nodes.remove(coordinate, node) != node) {
++ throw new IllegalStateException();
++ }
++ }
++
++ Thread unpark;
++ while ((unpark = node.pollOrBlockAdds()) != null) {
++ LockSupport.unpark(unpark);
++ }
++ }
++
++ public static final class Node extends MultiThreadedQueue<Thread> {
++
++ private final ReentrantAreaLock lock;
++ private final long[] areaAffected;
++ private int areaAffectedLen;
++ private final Thread thread;
++
++ private Node(final ReentrantAreaLock lock, final long[] areaAffected, final Thread thread) {
++ this.lock = lock;
++ this.areaAffected = areaAffected;
++ this.thread = thread;
++ }
++
++ @Override
++ public String toString() {
++ return "Node{" +
++ "areaAffected=" + IntPairUtil.toString(this.areaAffected, 0, this.areaAffectedLen) +
++ ", thread=" + this.thread +
++ '}';
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6918f130099e6c19e20a47bfdb54915cdd13732a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
+@@ -0,0 +1,1704 @@
++package ca.spottedleaf.concurrentutil.map;
++
++import ca.spottedleaf.concurrentutil.function.BiLong1Function;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.HashUtil;
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++import ca.spottedleaf.concurrentutil.util.ThrowUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import java.lang.invoke.VarHandle;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++import java.util.PrimitiveIterator;
++import java.util.concurrent.atomic.LongAdder;
++import java.util.function.BiFunction;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.LongConsumer;
++import java.util.function.LongFunction;
++import java.util.function.Predicate;
++
++/**
++ * Concurrent hashtable implementation supporting mapping arbitrary {@code long} values onto non-null {@code Object}
++ * values with support for multiple writer and multiple reader threads.
++ *
++ * <p><h3>Happens-before relationship</h3></p>
++ * <p>
++ * As with {@link java.util.concurrent.ConcurrentMap}, there is a happens-before relationship between actions in one thread
++ * prior to writing to the map and access to the results of those actions in another thread.
++ * </p>
++ *
++ * <p><h3>Atomicity of functional methods</h3></p>
++ * <p>
++ * Functional methods are functions declared in this class which possibly perform a write (remove, replace, or modify)
++ * to an entry in this map as a result of invoking a function on an input parameter. For example, {@link #compute(long, BiLong1Function)},
++ * {@link #merge(long, Object, BiFunction)} and {@link #removeIf(long, Predicate)} are examples of functional methods.
++ * Functional methods will be performed atomically, that is, the input parameter is guaranteed to only be invoked at most
++ * once per function call. The consequence of this behavior however is that a critical lock for a bin entry is held, which
++ * means that if the input parameter invocation makes additional calls to write into this hash table that the result
++ * is undefined and deadlock-prone.
++ * </p>
++ *
++ * @param <V>
++ * @see java.util.concurrent.ConcurrentMap
++ */
++public class ConcurrentLong2ReferenceChainedHashTable<V> implements Iterable<ConcurrentLong2ReferenceChainedHashTable.TableEntry<V>> {
++
++ protected static final int DEFAULT_CAPACITY = 16;
++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
++
++ protected final LongAdder size = new LongAdder();
++ protected final float loadFactor;
++
++ protected volatile TableEntry<V>[] table;
++
++ protected static final int THRESHOLD_NO_RESIZE = -1;
++ protected static final int THRESHOLD_RESIZING = -2;
++ protected volatile int threshold;
++ protected static final VarHandle THRESHOLD_HANDLE = ConcurrentUtil.getVarHandle(ConcurrentLong2ReferenceChainedHashTable.class, "threshold", int.class);
++
++ protected final int getThresholdAcquire() {
++ return (int)THRESHOLD_HANDLE.getAcquire(this);
++ }
++
++ protected final int getThresholdVolatile() {
++ return (int)THRESHOLD_HANDLE.getVolatile(this);
++ }
++
++ protected final void setThresholdPlain(final int threshold) {
++ THRESHOLD_HANDLE.set(this, threshold);
++ }
++
++ protected final void setThresholdRelease(final int threshold) {
++ THRESHOLD_HANDLE.setRelease(this, threshold);
++ }
++
++ protected final void setThresholdVolatile(final int threshold) {
++ THRESHOLD_HANDLE.setVolatile(this, threshold);
++ }
++
++ protected final int compareExchangeThresholdVolatile(final int expect, final int update) {
++ return (int)THRESHOLD_HANDLE.compareAndExchange(this, expect, update);
++ }
++
++ public ConcurrentLong2ReferenceChainedHashTable() {
++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
++ }
++
++ protected static int getTargetThreshold(final int capacity, final float loadFactor) {
++ final double ret = (double)capacity * (double)loadFactor;
++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
++ return THRESHOLD_NO_RESIZE;
++ }
++
++ return (int)Math.ceil(ret);
++ }
++
++ protected static int getCapacityFor(final int capacity) {
++ if (capacity <= 0) {
++ throw new IllegalArgumentException("Invalid capacity: " + capacity);
++ }
++ if (capacity >= MAXIMUM_CAPACITY) {
++ return MAXIMUM_CAPACITY;
++ }
++ return IntegerUtil.roundCeilLog2(capacity);
++ }
++
++ protected ConcurrentLong2ReferenceChainedHashTable(final int capacity, final float loadFactor) {
++ final int tableSize = getCapacityFor(capacity);
++
++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
++ }
++
++ if (tableSize == MAXIMUM_CAPACITY) {
++ this.setThresholdPlain(THRESHOLD_NO_RESIZE);
++ } else {
++ this.setThresholdPlain(getTargetThreshold(tableSize, loadFactor));
++ }
++
++ this.loadFactor = loadFactor;
++ // noinspection unchecked
++ this.table = (TableEntry<V>[])new TableEntry[tableSize];
++ }
++
++ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity) {
++ return createWithCapacity(capacity, DEFAULT_LOAD_FACTOR);
++ }
++
++ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity, final float loadFactor) {
++ return new ConcurrentLong2ReferenceChainedHashTable<>(capacity, loadFactor);
++ }
++
++ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected) {
++ return createWithExpected(expected, DEFAULT_LOAD_FACTOR);
++ }
++
++ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected, final float loadFactor) {
++ final int capacity = (int)Math.ceil((double)expected / (double)loadFactor);
++
++ return createWithCapacity(capacity, loadFactor);
++ }
++
++ /** must be deterministic given a key */
++ protected static int getHash(final long key) {
++ return (int)HashUtil.mix(key);
++ }
++
++ /**
++ * Returns the load factor associated with this map.
++ */
++ public final float getLoadFactor() {
++ return this.loadFactor;
++ }
++
++ protected static <V> TableEntry<V> getAtIndexVolatile(final TableEntry<V>[] table, final int index) {
++ //noinspection unchecked
++ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getVolatile(table, index);
++ }
++
++ protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
++ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
++ }
++
++ protected static <V> void setAtIndexVolatile(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
++ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setVolatile(table, index, value);
++ }
++
++ protected static <V> TableEntry<V> compareAndExchangeAtIndexVolatile(final TableEntry<V>[] table, final int index,
++ final TableEntry<V> expect, final TableEntry<V> update) {
++ //noinspection unchecked
++ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.compareAndExchange(table, index, expect, update);
++ }
++
++ /**
++ * Returns the possible node associated with the key, or {@code null} if there is no such node. The node
++ * returned may have a {@code null} {@link TableEntry#value}, in which case the node is a placeholder for
++ * a compute/computeIfAbsent call. The placeholder node should not be considered mapped in order to preserve
++ * happens-before relationships between writes and reads in the map.
++ */
++ protected final TableEntry<V> getNode(final long key) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ for (;;) {
++ TableEntry<V> node = getAtIndexVolatile(table, hash & (table.length - 1));
++
++ if (node == null) {
++ // node == null
++ return node;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue;
++ }
++
++ for (; node != null; node = node.getNextVolatile()) {
++ if (node.key == key) {
++ return node;
++ }
++ }
++
++ // node == null
++ return node;
++ }
++ }
++
++ /**
++ * Returns the currently mapped value associated with the specified key, or {@code null} if there is none.
++ *
++ * @param key Specified key
++ */
++ public V get(final long key) {
++ final TableEntry<V> node = this.getNode(key);
++ return node == null ? null : node.getValueVolatile();
++ }
++
++ /**
++ * Returns the currently mapped value associated with the specified key, or the specified default value if there is none.
++ *
++ * @param key Specified key
++ * @param defaultValue Specified default value
++ */
++ public V getOrDefault(final long key, final V defaultValue) {
++ final TableEntry<V> node = this.getNode(key);
++ if (node == null) {
++ return defaultValue;
++ }
++
++ final V ret = node.getValueVolatile();
++ if (ret == null) {
++ // ret == null for nodes pre-allocated to compute() and friends
++ return defaultValue;
++ }
++
++ return ret;
++ }
++
++ /**
++ * Returns whether the specified key is mapped to some value.
++ * @param key Specified key
++ */
++ public boolean containsKey(final long key) {
++ // cannot use getNode, as the node may be a placeholder for compute()
++ return this.get(key) != null;
++ }
++
++ /**
++ * Returns whether the specified value has a key mapped to it.
++ * @param value Specified value
++ * @throws NullPointerException If value is null
++ */
++ public boolean containsValue(final V value) {
++ Validate.notNull(value, "Value cannot be null");
++
++ final NodeIterator<V> iterator = new NodeIterator<>(this.table);
++
++ TableEntry<V> node;
++ while ((node = iterator.findNext()) != null) {
++ // need to use acquire here to ensure the happens-before relationship
++ if (node.getValueAcquire() == value) {
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ /**
++ * Returns the number of mappings in this map.
++ */
++ public int size() {
++ final long ret = this.size.sum();
++
++ if (ret < 0L) {
++ return 0;
++ }
++ if (ret > (long)Integer.MAX_VALUE) {
++ return Integer.MAX_VALUE;
++ }
++
++ return (int)ret;
++ }
++
++ /**
++ * Returns whether this map has no mappings.
++ */
++ public boolean isEmpty() {
++ return this.size.sum() <= 0L;
++ }
++
++ /**
++ * Adds count to size and checks threshold for resizing
++ */
++ protected final void addSize(final long count) {
++ this.size.add(count);
++
++ final int threshold = this.getThresholdAcquire();
++
++ if (threshold < 0L) {
++ // resizing or no resizing allowed, in either cases we do not need to do anything
++ return;
++ }
++
++ final long sum = this.size.sum();
++
++ if (sum < (long)threshold) {
++ return;
++ }
++
++ if (threshold != this.compareExchangeThresholdVolatile(threshold, THRESHOLD_RESIZING)) {
++ // some other thread resized
++ return;
++ }
++
++ // create new table
++ this.resize(sum);
++ }
++
++ /**
++ * Resizes table, only invoke for the thread which has successfully updated threshold to {@link #THRESHOLD_RESIZING}
++ * @param sum Estimate of current mapping count, must be >= old threshold
++ */
++ private void resize(final long sum) {
++ int capacity;
++
++ // add 1.0, as sum may equal threshold (in which case, sum / loadFactor = current capacity)
++ // adding 1.0 should at least raise the size by a factor of two due to usage of roundCeilLog2
++ final double targetD = ((double)sum / (double)this.loadFactor) + 1.0;
++ if (targetD >= (double)MAXIMUM_CAPACITY) {
++ capacity = MAXIMUM_CAPACITY;
++ } else {
++ capacity = (int)Math.ceil(targetD);
++ capacity = IntegerUtil.roundCeilLog2(capacity);
++ if (capacity > MAXIMUM_CAPACITY) {
++ capacity = MAXIMUM_CAPACITY;
++ }
++ }
++
++ // create new table data
++
++ // noinspection unchecked
++ final TableEntry<V>[] newTable = new TableEntry[capacity];
++ // noinspection unchecked
++ final TableEntry<V> resizeNode = new TableEntry<>(0L, (V)newTable, true);
++
++ // transfer nodes from old table
++
++ // does not need to be volatile read, just plain
++ final TableEntry<V>[] oldTable = this.table;
++
++ // when resizing, the old entries at bin i (where i = hash % oldTable.length) are assigned to
++ // bin k in the new table (where k = hash % newTable.length)
++ // since both table lengths are powers of two (specifically, newTable is a multiple of oldTable),
++ // the possible number of locations in the new table to assign any given i is newTable.length/oldTable.length
++
++ // we can build the new linked nodes for the new table by using a work array sized to newTable.length/oldTable.length
++ // which holds the _last_ entry in the chain per bin
++
++ final int capOldShift = IntegerUtil.floorLog2(oldTable.length);
++ final int capDiffShift = IntegerUtil.floorLog2(capacity) - capOldShift;
++
++ if (capDiffShift == 0) {
++ throw new IllegalStateException("Resizing to same size");
++ }
++
++ // noinspection unchecked
++ final TableEntry<V>[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1
++
++ for (int i = 0, len = oldTable.length; i < len; ++i) {
++ TableEntry<V> binNode = getAtIndexVolatile(oldTable, i);
++
++ for (;;) {
++ if (binNode == null) {
++ // just need to replace the bin node, do not need to move anything
++ if (null == (binNode = compareAndExchangeAtIndexVolatile(oldTable, i, null, resizeNode))) {
++ break;
++ } // else: binNode != null, fall through
++ }
++
++ // need write lock to block other writers
++ synchronized (binNode) {
++ if (binNode != (binNode = getAtIndexVolatile(oldTable, i))) {
++ continue;
++ }
++
++ // an important detail of resizing is that we do not need to be concerned with synchronisation on
++ // writes to the new table, as no access to any nodes on bin i on oldTable will occur until a thread
++ // sees the resizeNode
++ // specifically, as long as the resizeNode is release written there are no cases where another thread
++ // will see our writes to the new table
++
++ TableEntry<V> next = binNode.getNextPlain();
++
++ if (next == null) {
++ // simple case: do not use work array
++
++ // do not need to create new node, readers only need to see the state of the map at the
++ // beginning of a call, so any additions onto _next_ don't really matter
++ // additionally, the old node is replaced so that writers automatically forward to the new table,
++ // which resolves any issues
++ newTable[getHash(binNode.key) & (capacity - 1)] = binNode;
++ } else {
++ // reset for next usage
++ Arrays.fill(work, null);
++
++ for (TableEntry<V> curr = binNode; curr != null; curr = curr.getNextPlain()) {
++ final int newTableIdx = getHash(curr.key) & (capacity - 1);
++ final int workIdx = newTableIdx >>> capOldShift;
++
++ final TableEntry<V> replace = new TableEntry<>(curr.key, curr.getValuePlain());
++
++ final TableEntry<V> workNode = work[workIdx];
++ work[workIdx] = replace;
++
++ if (workNode == null) {
++ newTable[newTableIdx] = replace;
++ continue;
++ } else {
++ workNode.setNextPlain(replace);
++ continue;
++ }
++ }
++ }
++
++ setAtIndexRelease(oldTable, i, resizeNode);
++ break;
++ }
++ }
++ }
++
++ // calculate new threshold
++ final int newThreshold;
++ if (capacity == MAXIMUM_CAPACITY) {
++ newThreshold = THRESHOLD_NO_RESIZE;
++ } else {
++ newThreshold = getTargetThreshold(capacity, loadFactor);
++ }
++
++ this.table = newTable;
++ // finish resize operation by releasing hold on threshold
++ this.setThresholdVolatile(newThreshold);
++ }
++
++ /**
++ * Subtracts count from size
++ */
++ protected final void subSize(final long count) {
++ this.size.add(-count);
++ }
++
++ /**
++ * Atomically updates the value associated with {@code key} to {@code value}, or inserts a new mapping with {@code key}
++ * mapped to {@code value}.
++ * @param key Specified key
++ * @param value Specified value
++ * @throws NullPointerException If value is null
++ * @return Old value previously associated with key, or {@code null} if none.
++ */
++ public V put(final long key, final V value) {
++ Validate.notNull(value, "Value may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
++ // successfully inserted
++ this.addSize(1L);
++ return null;
++ } // else: node != null, fall through
++ }
++
++ if (node.resize) {
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V ret = node.getValuePlain();
++ node.setValueVolatile(value);
++ return ret;
++ }
++ }
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ prev.setNextRelease(new TableEntry<>(key, value));
++ }
++
++ this.addSize(1L);
++ return null;
++ }
++ }
++ }
++
++ /**
++ * Atomically inserts a new mapping with {@code key} mapped to {@code value} if and only if {@code key} is not
++ * currently mapped to some value.
++ * @param key Specified key
++ * @param value Specified value
++ * @throws NullPointerException If value is null
++ * @return Value currently associated with key, or {@code null} if none and {@code value} was associated.
++ */
++ public V putIfAbsent(final long key, final V value) {
++ Validate.notNull(value, "Value may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
++ // successfully inserted
++ this.addSize(1L);
++ return null;
++ } // else: node != null, fall through
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ // optimise ifAbsent calls: check if first node is key before attempting lock acquire
++ if (node.key == key) {
++ final V ret = node.getValueVolatile();
++ if (ret != null) {
++ return ret;
++ } // else: fall back to lock to read the node
++ }
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ return node.getValuePlain();
++ }
++ }
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ prev.setNextRelease(new TableEntry<>(key, value));
++ }
++
++ this.addSize(1L);
++ return null;
++ }
++ }
++ }
++
++ /**
++ * Atomically updates the value associated with {@code key} to {@code value}, or does nothing if {@code key} is not
++ * associated with a value.
++ * @param key Specified key
++ * @param value Specified value
++ * @throws NullPointerException If value is null
++ * @return Old value previously associated with key, or {@code null} if none.
++ */
++ public V replace(final long key, final V value) {
++ Validate.notNull(value, "Value may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++
++ // plain reads are fine during synchronised access, as we are the only writer
++ for (; node != null; node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V ret = node.getValuePlain();
++ node.setValueVolatile(value);
++ return ret;
++ }
++ }
++ }
++
++ return null;
++ }
++ }
++ }
++
++ /**
++ * Atomically updates the value associated with {@code key} to {@code update} if the currently associated
++ * value is reference equal to {@code expect}, otherwise does nothing.
++ * @param key Specified key
++ * @param expect Expected value to check current mapped value with
++ * @param update Update value to replace mapped value with
++ * @throws NullPointerException If value is null
++ * @return If the currently mapped value is not reference equal to {@code expect}, then returns the currently mapped
++ * value. If the key is not mapped to any value, then returns {@code null}. If neither of the two cases are
++ * true, then returns {@code expect}.
++ */
++ public V replace(final long key, final V expect, final V update) {
++ Validate.notNull(expect, "Expect may not be null");
++ Validate.notNull(update, "Update may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++
++ // plain reads are fine during synchronised access, as we are the only writer
++ for (; node != null; node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V ret = node.getValuePlain();
++
++ if (ret != expect) {
++ return ret;
++ }
++
++ node.setValueVolatile(update);
++ return ret;
++ }
++ }
++ }
++
++ return null;
++ }
++ }
++ }
++
++ /**
++ * Atomically removes the mapping for the specified key and returns the value it was associated with. If the key
++ * is not mapped to a value, then does nothing and returns {@code null}.
++ * @param key Specified key
++ * @return Old value previously associated with key, or {@code null} if none.
++ */
++ public V remove(final long key) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++ V ret = null;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++
++ TableEntry<V> prev = null;
++
++ // plain reads are fine during synchronised access, as we are the only writer
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ ret = node.getValuePlain();
++ removed = true;
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++
++ break;
++ }
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * Atomically removes the mapping for the specified key if it is mapped to {@code expect} and returns {@code expect}. If the key
++ * is not mapped to a value, then does nothing and returns {@code null}. If the key is mapped to a value that is not reference
++ * equal to {@code expect}, then returns that value.
++ * @param key Specified key
++ * @param expect Specified expected value
++ * @return The specified expected value if the key was mapped to {@code expect}. If
++ * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
++ * then returns the current (non-null) mapped value for key.
++ */
++ public V remove(final long key, final V expect) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++ V ret = null;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++
++ TableEntry<V> prev = null;
++
++ // plain reads are fine during synchronised access, as we are the only writer
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ ret = node.getValuePlain();
++ if (ret == expect) {
++ removed = true;
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++ }
++ break;
++ }
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * Atomically removes the mapping for the specified key the predicate returns true for its currently mapped value. If the key
++ * is not mapped to a value, then does nothing and returns {@code null}.
++ *
++ * <p>
++ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
++ * </p>
++ *
++ * @param key Specified key
++ * @param predicate Specified predicate
++ * @throws NullPointerException If predicate is null
++ * @return The specified expected value if the key was mapped to {@code expect}. If
++ * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
++ * then returns the current (non-null) mapped value for key.
++ */
++ public V removeIf(final long key, final Predicate<? super V> predicate) {
++ Validate.notNull(predicate, "Predicate may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++ V ret = null;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++
++ TableEntry<V> prev = null;
++
++ // plain reads are fine during synchronised access, as we are the only writer
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ ret = node.getValuePlain();
++ if (predicate.test(ret)) {
++ removed = true;
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++ }
++ break;
++ }
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * See {@link java.util.concurrent.ConcurrentMap#compute(Object, BiFunction)}
++ * <p>
++ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
++ * </p>
++ */
++ public V compute(final long key, final BiLong1Function<? super V, ? extends V> function) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ V ret = null;
++ if (node == null) {
++ final TableEntry<V> insert = new TableEntry<>(key, null);
++
++ boolean added = false;
++
++ synchronized (insert) {
++ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
++ try {
++ ret = function.apply(key, null);
++ } catch (final Throwable throwable) {
++ setAtIndexVolatile(table, index, null);
++ ThrowUtil.throwUnchecked(throwable);
++ // unreachable
++ return null;
++ }
++
++ if (ret == null) {
++ setAtIndexVolatile(table, index, null);
++ return ret;
++ } else {
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ insert.setValueRelease(ret);
++ added = true;
++ }
++ } // else: node != null, fall through
++ }
++
++ if (added) {
++ this.addSize(1L);
++ return ret;
++ }
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++ boolean added = false;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V old = node.getValuePlain();
++
++ final V computed = function.apply(key, old);
++
++ if (computed != null) {
++ node.setValueVolatile(computed);
++ return computed;
++ }
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++
++ removed = true;
++ break;
++ }
++ }
++
++ if (!removed) {
++ final V computed = function.apply(key, null);
++ if (computed != null) {
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ prev.setNextRelease(new TableEntry<>(key, computed));
++ ret = computed;
++ added = true;
++ }
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++ if (added) {
++ this.addSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * See {@link java.util.concurrent.ConcurrentMap#computeIfAbsent(Object, Function)}
++ * <p>
++ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
++ * </p>
++ */
++ public V computeIfAbsent(final long key, final LongFunction<? extends V> function) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ V ret = null;
++ if (node == null) {
++ final TableEntry<V> insert = new TableEntry<>(key, null);
++
++ boolean added = false;
++
++ synchronized (insert) {
++ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
++ try {
++ ret = function.apply(key);
++ } catch (final Throwable throwable) {
++ setAtIndexVolatile(table, index, null);
++ ThrowUtil.throwUnchecked(throwable);
++ // unreachable
++ return null;
++ }
++
++ if (ret == null) {
++ setAtIndexVolatile(table, index, null);
++ return null;
++ } else {
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ insert.setValueRelease(ret);
++ added = true;
++ }
++ } // else: node != null, fall through
++ }
++
++ if (added) {
++ this.addSize(1L);
++ return ret;
++ }
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ // optimise ifAbsent calls: check if first node is key before attempting lock acquire
++ if (node.key == key) {
++ ret = node.getValueVolatile();
++ if (ret != null) {
++ return ret;
++ } // else: fall back to lock to read the node
++ }
++
++ boolean added = false;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ ret = node.getValuePlain();
++ return ret;
++ }
++ }
++
++ final V computed = function.apply(key);
++ if (computed != null) {
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ prev.setNextRelease(new TableEntry<>(key, computed));
++ ret = computed;
++ added = true;
++ }
++ }
++
++ if (added) {
++ this.addSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * See {@link java.util.concurrent.ConcurrentMap#computeIfPresent(Object, BiFunction)}
++ * <p>
++ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
++ * </p>
++ */
++ public V computeIfPresent(final long key, final BiLong1Function<? super V, ? extends V> function) {
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ return null;
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V old = node.getValuePlain();
++
++ final V computed = function.apply(key, old);
++
++ if (computed != null) {
++ node.setValueVolatile(computed);
++ return computed;
++ }
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++
++ removed = true;
++ break;
++ }
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++
++ return null;
++ }
++ }
++ }
++
++ /**
++ * See {@link java.util.concurrent.ConcurrentMap#merge(Object, Object, BiFunction)}
++ * <p>
++ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
++ * </p>
++ */
++ public V merge(final long key, final V def, final BiFunction<? super V, ? super V, ? extends V> function) {
++ Validate.notNull(def, "Default value may not be null");
++
++ final int hash = getHash(key);
++
++ TableEntry<V>[] table = this.table;
++ table_loop:
++ for (;;) {
++ final int index = hash & (table.length - 1);
++
++ TableEntry<V> node = getAtIndexVolatile(table, index);
++ node_loop:
++ for (;;) {
++ if (node == null) {
++ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, def)))) {
++ // successfully inserted
++ this.addSize(1L);
++ return def;
++ } // else: node != null, fall through
++ }
++
++ if (node.resize) {
++ // noinspection unchecked
++ table = (TableEntry<V>[])node.getValuePlain();
++ continue table_loop;
++ }
++
++ boolean removed = false;
++ boolean added = false;
++ V ret = null;
++
++ synchronized (node) {
++ if (node != (node = getAtIndexVolatile(table, index))) {
++ continue node_loop;
++ }
++ // plain reads are fine during synchronised access, as we are the only writer
++ TableEntry<V> prev = null;
++ for (; node != null; prev = node, node = node.getNextPlain()) {
++ if (node.key == key) {
++ final V old = node.getValuePlain();
++
++ final V computed = function.apply(old, def);
++
++ if (computed != null) {
++ node.setValueVolatile(computed);
++ return computed;
++ }
++
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ if (prev == null) {
++ setAtIndexRelease(table, index, node.getNextPlain());
++ } else {
++ prev.setNextRelease(node.getNextPlain());
++ }
++
++ removed = true;
++ break;
++ }
++ }
++
++ if (!removed) {
++ // volatile ordering ensured by addSize(), but we need release here
++ // to ensure proper ordering with reads and other writes
++ prev.setNextRelease(new TableEntry<>(key, def));
++ ret = def;
++ added = true;
++ }
++ }
++
++ if (removed) {
++ this.subSize(1L);
++ }
++ if (added) {
++ this.addSize(1L);
++ }
++
++ return ret;
++ }
++ }
++ }
++
++ /**
++ * Removes at least all entries currently mapped at the beginning of this call. May not remove entries added during
++ * this call. As a result, only if this map is not modified during the call, that all entries will be removed by
++ * the end of the call.
++ *
++ * <p>
++ * This function is not atomic.
++ * </p>
++ */
++ public void clear() {
++ // it is possible to optimise this to directly interact with the table,
++ // but we do need to be careful when interacting with resized tables,
++ // and the NodeIterator already does this logic
++ final NodeIterator<V> nodeIterator = new NodeIterator<>(this.table);
++
++ TableEntry<V> node;
++ while ((node = nodeIterator.findNext()) != null) {
++ this.remove(node.key);
++ }
++ }
++
++ /**
++ * Returns an iterator over the entries in this map. The iterator is only guaranteed to see entries that were
++ * added before the beginning of this call, but it may see entries added during.
++ */
++ public Iterator<TableEntry<V>> entryIterator() {
++ return new EntryIterator<>(this);
++ }
++
++ @Override
++ public final Iterator<TableEntry<V>> iterator() {
++ return this.entryIterator();
++ }
++
++ /**
++ * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were
++ * added before the beginning of this call, but it may see keys added during.
++ */
++ public PrimitiveIterator.OfLong keyIterator() {
++ return new KeyIterator<>(this);
++ }
++
++ /**
++ * Returns an iterator over the values in this map. The iterator is only guaranteed to see values that were
++ * added before the beginning of this call, but it may see values added during.
++ */
++ public Iterator<V> valueIterator() {
++ return new ValueIterator<>(this);
++ }
++
++ protected static final class EntryIterator<V> extends BaseIteratorImpl<V, TableEntry<V>> {
++
++ public EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++ super(map);
++ }
++
++ @Override
++ public TableEntry<V> next() throws NoSuchElementException {
++ return this.nextNode();
++ }
++
++ @Override
++ public void forEachRemaining(final Consumer<? super TableEntry<V>> action) {
++ Validate.notNull(action, "Action may not be null");
++ while (this.hasNext()) {
++ action.accept(this.next());
++ }
++ }
++ }
++
++ protected static final class KeyIterator<V> extends BaseIteratorImpl<V, Long> implements PrimitiveIterator.OfLong {
++
++ public KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++ super(map);
++ }
++
++ @Override
++ public Long next() throws NoSuchElementException {
++ return Long.valueOf(this.nextNode().key);
++ }
++
++ @Override
++ public long nextLong() {
++ return this.nextNode().key;
++ }
++
++ @Override
++ public void forEachRemaining(final Consumer<? super Long> action) {
++ Validate.notNull(action, "Action may not be null");
++
++ if (action instanceof LongConsumer longConsumer) {
++ this.forEachRemaining(longConsumer);
++ return;
++ }
++
++ while (this.hasNext()) {
++ action.accept(this.next());
++ }
++ }
++
++ @Override
++ public void forEachRemaining(final LongConsumer action) {
++ Validate.notNull(action, "Action may not be null");
++ while (this.hasNext()) {
++ action.accept(this.nextLong());
++ }
++ }
++ }
++
++ protected static final class ValueIterator<V> extends BaseIteratorImpl<V, V> {
++
++ public ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++ super(map);
++ }
++
++ @Override
++ public V next() throws NoSuchElementException {
++ return this.nextNode().getValueVolatile();
++ }
++
++ @Override
++ public void forEachRemaining(final Consumer<? super V> action) {
++ Validate.notNull(action, "Action may not be null");
++ while (this.hasNext()) {
++ action.accept(this.next());
++ }
++ }
++ }
++
++ protected static abstract class BaseIteratorImpl<V, T> extends NodeIterator<V> implements Iterator<T> {
++
++ protected final ConcurrentLong2ReferenceChainedHashTable<V> map;
++ protected TableEntry<V> lastReturned;
++ protected TableEntry<V> nextToReturn;
++
++ protected BaseIteratorImpl(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++ super(map.table);
++ this.map = map;
++ }
++
++ @Override
++ public final boolean hasNext() {
++ if (this.nextToReturn != null) {
++ return true;
++ }
++
++ return (this.nextToReturn = this.findNext()) != null;
++ }
++
++ protected final TableEntry<V> nextNode() throws NoSuchElementException {
++ TableEntry<V> ret = this.nextToReturn;
++ if (ret != null) {
++ this.lastReturned = ret;
++ this.nextToReturn = null;
++ return ret;
++ }
++ ret = this.findNext();
++ if (ret != null) {
++ this.lastReturned = ret;
++ return ret;
++ }
++ throw new NoSuchElementException();
++ }
++
++ @Override
++ public final void remove() {
++ final TableEntry<V> lastReturned = this.lastReturned;
++ if (lastReturned == null) {
++ throw new NoSuchElementException();
++ }
++ this.lastReturned = null;
++ this.map.remove(lastReturned.key);
++ }
++
++ @Override
++ public abstract T next() throws NoSuchElementException;
++
++ // overwritten by subclasses to avoid indirection on hasNext() and next()
++ @Override
++ public abstract void forEachRemaining(final Consumer<? super T> action);
++ }
++
++ protected static class NodeIterator<V> {
++
++ protected TableEntry<V>[] currentTable;
++ protected ResizeChain<V> resizeChain;
++ protected TableEntry<V> last;
++ protected int nextBin;
++ protected int increment;
++
++ protected NodeIterator(final TableEntry<V>[] baseTable) {
++ this.currentTable = baseTable;
++ this.increment = 1;
++ }
++
++ private TableEntry<V>[] pullResizeChain(final int index) {
++ final ResizeChain<V> resizeChain = this.resizeChain;
++ if (resizeChain == null) {
++ this.currentTable = null;
++ return null;
++ }
++
++ final ResizeChain<V> prevChain = resizeChain.prev;
++ this.resizeChain = prevChain;
++ if (prevChain == null) {
++ this.currentTable = null;
++ return null;
++ }
++
++ final TableEntry<V>[] newTable = prevChain.table;
++
++ // we recover the original index by modding by the new table length, as the increments applied to the index
++ // are a multiple of the new table's length
++ int newIdx = index & (newTable.length - 1);
++
++ // the increment is always the previous table's length
++ final ResizeChain<V> nextPrevChain = prevChain.prev;
++ final int increment;
++ if (nextPrevChain == null) {
++ increment = 1;
++ } else {
++ increment = nextPrevChain.table.length;
++ }
++
++ // done with the upper table, so we can skip the resize node
++ newIdx += increment;
++
++ this.increment = increment;
++ this.nextBin = newIdx;
++ this.currentTable = newTable;
++
++ return newTable;
++ }
++
++ private TableEntry<V>[] pushResizeChain(final TableEntry<V>[] table, final TableEntry<V> entry) {
++ final ResizeChain<V> chain = this.resizeChain;
++
++ if (chain == null) {
++ // noinspection unchecked
++ final TableEntry<V>[] nextTable = (TableEntry<V>[])entry.getValuePlain();
++
++ final ResizeChain<V> oldChain = new ResizeChain<>(table, null, null);
++ final ResizeChain<V> currChain = new ResizeChain<>(nextTable, oldChain, null);
++ oldChain.next = currChain;
++
++ this.increment = table.length;
++ this.resizeChain = currChain;
++ this.currentTable = nextTable;
++
++ return nextTable;
++ } else {
++ ResizeChain<V> currChain = chain.next;
++ if (currChain == null) {
++ // noinspection unchecked
++ final TableEntry<V>[] ret = (TableEntry<V>[])entry.getValuePlain();
++ currChain = new ResizeChain<>(ret, chain, null);
++ chain.next = currChain;
++
++ this.increment = table.length;
++ this.resizeChain = currChain;
++ this.currentTable = ret;
++
++ return ret;
++ } else {
++ this.increment = table.length;
++ this.resizeChain = currChain;
++ return this.currentTable = currChain.table;
++ }
++ }
++ }
++
++ protected final TableEntry<V> findNext() {
++ for (;;) {
++ final TableEntry<V> last = this.last;
++ if (last != null) {
++ final TableEntry<V> next = last.getNextVolatile();
++ if (next != null) {
++ this.last = next;
++ if (next.getValuePlain() == null) {
++ // compute() node not yet available
++ continue;
++ }
++ return next;
++ }
++ }
++
++ TableEntry<V>[] table = this.currentTable;
++
++ if (table == null) {
++ return null;
++ }
++
++ int idx = this.nextBin;
++ int increment = this.increment;
++ for (;;) {
++ if (idx >= table.length) {
++ table = this.pullResizeChain(idx);
++ idx = this.nextBin;
++ increment = this.increment;
++ if (table != null) {
++ continue;
++ } else {
++ this.last = null;
++ return null;
++ }
++ }
++
++ final TableEntry<V> entry = getAtIndexVolatile(table, idx);
++ if (entry == null) {
++ idx += increment;
++ continue;
++ }
++
++ if (entry.resize) {
++ // push onto resize chain
++ table = this.pushResizeChain(table, entry);
++ increment = this.increment;
++ continue;
++ }
++
++ this.last = entry;
++ this.nextBin = idx + increment;
++ if (entry.getValuePlain() != null) {
++ return entry;
++ } else {
++ // compute() node not yet available
++ break;
++ }
++ }
++ }
++ }
++
++ protected static final class ResizeChain<V> {
++
++ public final TableEntry<V>[] table;
++ public final ResizeChain<V> prev;
++ public ResizeChain<V> next;
++
++ public ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) {
++ this.table = table;
++ this.prev = prev;
++ this.next = next;
++ }
++ }
++ }
++
++ public static final class TableEntry<V> {
++
++ private static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
++
++ private final boolean resize;
++
++ private final long key;
++
++ private volatile V value;
++ private static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++
++ private V getValuePlain() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.get(this);
++ }
++
++ private V getValueAcquire() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.getAcquire(this);
++ }
++
++ private V getValueVolatile() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.getVolatile(this);
++ }
++
++ private void setValuePlain(final V value) {
++ VALUE_HANDLE.set(this, (Object)value);
++ }
++
++ private void setValueRelease(final V value) {
++ VALUE_HANDLE.setRelease(this, (Object)value);
++ }
++
++ private void setValueVolatile(final V value) {
++ VALUE_HANDLE.setVolatile(this, (Object)value);
++ }
++
++ private volatile TableEntry<V> next;
++ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++
++ private TableEntry<V> getNextPlain() {
++ //noinspection unchecked
++ return (TableEntry<V>)NEXT_HANDLE.get(this);
++ }
++
++ private TableEntry<V> getNextVolatile() {
++ //noinspection unchecked
++ return (TableEntry<V>)NEXT_HANDLE.getVolatile(this);
++ }
++
++ private void setNextPlain(final TableEntry<V> next) {
++ NEXT_HANDLE.set(this, next);
++ }
++
++ private void setNextRelease(final TableEntry<V> next) {
++ NEXT_HANDLE.setRelease(this, next);
++ }
++
++ private void setNextVolatile(final TableEntry<V> next) {
++ NEXT_HANDLE.setVolatile(this, next);
++ }
++
++ public TableEntry(final long key, final V value) {
++ this.resize = false;
++ this.key = key;
++ this.setValuePlain(value);
++ }
++
++ public TableEntry(final long key, final V value, final boolean resize) {
++ this.resize = resize;
++ this.key = key;
++ this.setValuePlain(value);
++ }
++
++ public long getKey() {
++ return this.key;
++ }
++
++ public V getValue() {
++ return this.getValueVolatile();
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..83965350d292ccf42a34520d84dcda3f88146cff
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
+@@ -0,0 +1,1656 @@
++package ca.spottedleaf.concurrentutil.map;
++
++import ca.spottedleaf.concurrentutil.util.CollectionUtil;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.HashUtil;
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import java.lang.invoke.VarHandle;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.NoSuchElementException;
++import java.util.Set;
++import java.util.Spliterator;
++import java.util.Spliterators;
++import java.util.function.BiConsumer;
++import java.util.function.BiFunction;
++import java.util.function.BiPredicate;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.IntFunction;
++import java.util.function.Predicate;
++
++/**
++ * <p>
++ * Note: Not really tested, use at your own risk.
++ * </p>
++ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread.
++ * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics,
++ * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually.
++ * Reads are ordered by acquire semantics.
++ * <p>
++ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no
++ * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined
++ * behaviour.
++ * </p>
++ * <p>
++ * Subclasses should override {@link #clone()} to return correct instances of this class.
++ * </p>
++ * @param <K> {@inheritDoc}
++ * @param <V> {@inheritDoc}
++ */
++public class SWMRHashTable<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> {
++
++ protected int size;
++
++ protected TableEntry<K, V>[] table;
++
++ protected final float loadFactor;
++
++ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class);
++ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class);
++
++ /* size */
++
++ protected final int getSizePlain() {
++ return (int)SIZE_HANDLE.get(this);
++ }
++
++ protected final int getSizeOpaque() {
++ return (int)SIZE_HANDLE.getOpaque(this);
++ }
++
++ protected final int getSizeAcquire() {
++ return (int)SIZE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setSizePlain(final int value) {
++ SIZE_HANDLE.set(this, value);
++ }
++
++ protected final void setSizeOpaque(final int value) {
++ SIZE_HANDLE.setOpaque(this, value);
++ }
++
++ protected final void setSizeRelease(final int value) {
++ SIZE_HANDLE.setRelease(this, value);
++ }
++
++ /* table */
++
++ protected final TableEntry<K, V>[] getTablePlain() {
++ //noinspection unchecked
++ return (TableEntry<K, V>[])TABLE_HANDLE.get(this);
++ }
++
++ protected final TableEntry<K, V>[] getTableAcquire() {
++ //noinspection unchecked
++ return (TableEntry<K, V>[])TABLE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setTablePlain(final TableEntry<K, V>[] table) {
++ TABLE_HANDLE.set(this, table);
++ }
++
++ protected final void setTableRelease(final TableEntry<K, V>[] table) {
++ TABLE_HANDLE.setRelease(this, table);
++ }
++
++ protected static final int DEFAULT_CAPACITY = 16;
++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
++
++ /**
++ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
++ */
++ public SWMRHashTable() {
++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
++ }
++
++ /**
++ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
++ * @param capacity specified initial capacity, > 0
++ */
++ public SWMRHashTable(final int capacity) {
++ this(capacity, DEFAULT_LOAD_FACTOR);
++ }
++
++ /**
++ * Constructs this map with the specified capacity and load factor.
++ * @param capacity specified capacity, > 0
++ * @param loadFactor specified load factor, > 0 && finite
++ */
++ public SWMRHashTable(final int capacity, final float loadFactor) {
++ final int tableSize = getCapacityFor(capacity);
++
++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
++ }
++
++ //noinspection unchecked
++ final TableEntry<K, V>[] table = new TableEntry[tableSize];
++ this.setTablePlain(table);
++
++ if (tableSize == MAXIMUM_CAPACITY) {
++ this.threshold = -1;
++ } else {
++ this.threshold = getTargetCapacity(tableSize, loadFactor);
++ }
++
++ this.loadFactor = loadFactor;
++ }
++
++ /**
++ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
++ * with a load factor of {@code 0.75f}.
++ * All of the specified map's entries are copied into this map.
++ * @param other The specified map.
++ */
++ public SWMRHashTable(final Map<K, V> other) {
++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
++ }
++
++ /**
++ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
++ * with a load factor of {@code 0.75f}.
++ * All of the specified map's entries are copied into this map.
++ * @param capacity specified capacity, > 0
++ * @param other The specified map.
++ */
++ public SWMRHashTable(final int capacity, final Map<K, V> other) {
++ this(capacity, DEFAULT_LOAD_FACTOR, other);
++ }
++
++ /**
++ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
++ * with the specified load factor.
++ * All of the specified map's entries are copied into this map.
++ * @param capacity specified capacity, > 0
++ * @param loadFactor specified load factor, > 0 && finite
++ * @param other The specified map.
++ */
++ public SWMRHashTable(final int capacity, final float loadFactor, final Map<K, V> other) {
++ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
++ this.putAll(other);
++ }
++
++ protected static <K, V> TableEntry<K, V> getAtIndexOpaque(final TableEntry<K, V>[] table, final int index) {
++ // noinspection unchecked
++ return (TableEntry<K, V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
++ }
++
++ protected static <K, V> void setAtIndexRelease(final TableEntry<K, V>[] table, final int index, final TableEntry<K, V> value) {
++ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
++ }
++
++ public final float getLoadFactor() {
++ return this.loadFactor;
++ }
++
++ protected static int getCapacityFor(final int capacity) {
++ if (capacity <= 0) {
++ throw new IllegalArgumentException("Invalid capacity: " + capacity);
++ }
++ if (capacity >= MAXIMUM_CAPACITY) {
++ return MAXIMUM_CAPACITY;
++ }
++ return IntegerUtil.roundCeilLog2(capacity);
++ }
++
++ /** Callers must still use acquire when reading the value of the entry. */
++ protected final TableEntry<K, V> getEntryForOpaque(final K key) {
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
++ return curr;
++ }
++ }
++
++ return null;
++ }
++
++ protected final TableEntry<K, V> getEntryForPlain(final K key) {
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTablePlain();
++
++ for (TableEntry<K, V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
++ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
++ return curr;
++ }
++ }
++
++ return null;
++ }
++
++ /* MT-Safe */
++
++ /** must be deterministic given a key */
++ private static int getHash(final Object key) {
++ int hash = key == null ? 0 : key.hashCode();
++ return HashUtil.mix(hash);
++ }
++
++ // rets -1 if capacity*loadFactor is too large
++ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
++ final double ret = (double)capacity * (double)loadFactor;
++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
++ return -1;
++ }
++
++ return (int)ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean equals(final Object obj) {
++ if (this == obj) {
++ return true;
++ }
++ /* Make no attempt to deal with concurrent modifications */
++ if (!(obj instanceof Map<?, ?> other)) {
++ return false;
++ }
++
++ if (this.size() != other.size()) {
++ return false;
++ }
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ final Object otherValue = other.get(curr.key);
++ if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public int hashCode() {
++ /* Make no attempt to deal with concurrent modifications */
++ int hash = 0;
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ hash += curr.hashCode();
++ }
++ }
++
++ return hash;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public String toString() {
++ final StringBuilder builder = new StringBuilder(64);
++ builder.append("SWMRHashTable:{");
++
++ this.forEach((final K key, final V value) -> {
++ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
++ });
++
++ return builder.append('}').toString();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public SWMRHashTable<K, V> clone() {
++ return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public Iterator<Map.Entry<K, V>> iterator() {
++ return new EntryIterator<>(this.getTableAcquire(), this);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void forEach(final Consumer<? super Map.Entry<K, V>> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ action.accept(curr);
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void forEach(final BiConsumer<? super K, ? super V> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ action.accept(curr.key, value);
++ }
++ }
++ }
++
++ /**
++ * Provides the specified consumer with all keys contained within this map.
++ * @param action The specified consumer.
++ */
++ public void forEachKey(final Consumer<? super K> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ action.accept(curr.key);
++ }
++ }
++ }
++
++ /**
++ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
++ * @param action The specified consumer.
++ */
++ public void forEachValue(final Consumer<? super V> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ action.accept(value);
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V get(final Object key) {
++ Validate.notNull(key, "Null key");
++
++ //noinspection unchecked
++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++ return entry == null ? null : entry.getValueAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean containsKey(final Object key) {
++ Validate.notNull(key, "Null key");
++
++ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
++ return this.get(key) != null;
++ }
++
++ /**
++ * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call.
++ * @param key The specified key.
++ * @param value The specified value.
++ * @return {@code true} if this map contains an entry with the specified key and value.
++ */
++ public boolean contains(final Object key, final Object value) {
++ Validate.notNull(key, "Null key");
++
++ //noinspection unchecked
++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++
++ if (entry == null) {
++ return false;
++ }
++
++ final V entryVal = entry.getValueAcquire();
++ return entryVal == value || entryVal.equals(value);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean containsValue(final Object value) {
++ Validate.notNull(value, "Null value");
++
++ final TableEntry<K, V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V currVal = curr.getValueAcquire();
++ if (currVal == value || currVal.equals(value)) {
++ return true;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V getOrDefault(final Object key, final V defaultValue) {
++ Validate.notNull(key, "Null key");
++
++ //noinspection unchecked
++ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++
++ return entry == null ? defaultValue : entry.getValueAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public int size() {
++ return this.getSizeAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean isEmpty() {
++ return this.getSizeAcquire() == 0;
++ }
++
++ protected KeySet<K, V> keyset;
++ protected ValueCollection<K, V> values;
++ protected EntrySet<K, V> entrySet;
++
++ @Override
++ public Set<K> keySet() {
++ return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset;
++ }
++
++ @Override
++ public Collection<V> values() {
++ return this.values == null ? this.values = new ValueCollection<>(this) : this.values;
++ }
++
++ @Override
++ public Set<Map.Entry<K, V>> entrySet() {
++ return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet;
++ }
++
++ /* Non-MT-Safe */
++
++ protected int threshold;
++
++ protected final void checkResize(final int minCapacity) {
++ if (minCapacity <= this.threshold || this.threshold < 0) {
++ return;
++ }
++
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
++ if (newCapacity < 0) {
++ newCapacity = MAXIMUM_CAPACITY;
++ }
++ if (newCapacity <= table.length) {
++ if (newCapacity == MAXIMUM_CAPACITY) {
++ return;
++ }
++ newCapacity = table.length << 1;
++ }
++
++ //noinspection unchecked
++ final TableEntry<K, V>[] newTable = new TableEntry[newCapacity];
++ final int indexMask = newCapacity - 1;
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
++ final int hash = entry.hash;
++ final int index = hash & indexMask;
++
++ /* we need to create a new entry since there could be reading threads */
++ final TableEntry<K, V> insert = new TableEntry<>(hash, entry.key, entry.getValuePlain());
++
++ final TableEntry<K, V> prev = newTable[index];
++
++ newTable[index] = insert;
++ insert.setNextPlain(prev);
++ }
++ }
++
++ if (newCapacity == MAXIMUM_CAPACITY) {
++ this.threshold = -1; /* No more resizing */
++ } else {
++ this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
++ }
++ this.setTableRelease(newTable); /* use release to publish entries in table */
++ }
++
++ protected final int addToSize(final int num) {
++ final int newSize = this.getSizePlain() + num;
++
++ this.setSizeOpaque(newSize);
++ this.checkResize(newSize);
++
++ return newSize;
++ }
++
++ protected final int removeFromSize(final int num) {
++ final int newSize = this.getSizePlain() - num;
++
++ this.setSizeOpaque(newSize);
++
++ return newSize;
++ }
++
++ /* Cannot be used to perform downsizing */
++ protected final int removeFromSizePlain(final int num) {
++ final int newSize = this.getSizePlain() - num;
++
++ this.setSizePlain(newSize);
++
++ return newSize;
++ }
++
++ protected final V put(final K key, final V value, final boolean onlyIfAbsent) {
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int hash = SWMRHashTable.getHash(key);
++ final int index = hash & (table.length - 1);
++
++ final TableEntry<K, V> head = table[index];
++ if (head == null) {
++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++ setAtIndexRelease(table, index, insert);
++ this.addToSize(1);
++ return null;
++ }
++
++ for (TableEntry<K, V> curr = head;;) {
++ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
++ if (onlyIfAbsent) {
++ return curr.getValuePlain();
++ }
++
++ final V currVal = curr.getValuePlain();
++ curr.setValueRelease(value);
++ return currVal;
++ }
++
++ final TableEntry<K, V> next = curr.getNextPlain();
++ if (next != null) {
++ curr = next;
++ continue;
++ }
++
++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++
++ curr.setNextRelease(insert);
++ this.addToSize(1);
++ return null;
++ }
++ }
++
++ /**
++ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
++ * tested with every entry in this map. Returns the number of key-value pairs removed.
++ * @param predicate The predicate to test key-value pairs against.
++ * @return The total number of key-value pairs removed from this map.
++ */
++ public int removeIf(final BiPredicate<K, V> predicate) {
++ Validate.notNull(predicate, "Null predicate");
++
++ int removed = 0;
++
++ final TableEntry<K, V>[] table = this.getTablePlain();
++
++ bin_iteration_loop:
++ for (int i = 0, len = table.length; i < len; ++i) {
++ TableEntry<K, V> curr = table[i];
++ if (curr == null) {
++ continue;
++ }
++
++ /* Handle bin nodes first */
++ while (predicate.test(curr.key, curr.getValuePlain())) {
++ ++removed;
++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++ setAtIndexRelease(table, i, curr = curr.getNextPlain());
++
++ if (curr == null) {
++ continue bin_iteration_loop;
++ }
++ }
++
++ TableEntry<K, V> prev;
++
++ /* curr at this point is the bin node */
++
++ for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
++ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
++ if (predicate.test(curr.key, curr.getValuePlain())) {
++ ++removed;
++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++ prev.setNextRelease(curr = curr.getNextPlain());
++ } else {
++ prev = curr;
++ curr = curr.getNextPlain();
++ }
++ }
++ }
++
++ return removed;
++ }
++
++ /**
++ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
++ * tested with every entry in this map. Returns the number of key-value pairs removed.
++ * @param predicate The predicate to test key-value pairs against.
++ * @return The total number of key-value pairs removed from this map.
++ */
++ public int removeEntryIf(final Predicate<? super Map.Entry<K, V>> predicate) {
++ Validate.notNull(predicate, "Null predicate");
++
++ int removed = 0;
++
++ final TableEntry<K, V>[] table = this.getTablePlain();
++
++ bin_iteration_loop:
++ for (int i = 0, len = table.length; i < len; ++i) {
++ TableEntry<K, V> curr = table[i];
++ if (curr == null) {
++ continue;
++ }
++
++ /* Handle bin nodes first */
++ while (predicate.test(curr)) {
++ ++removed;
++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++ setAtIndexRelease(table, i, curr = curr.getNextPlain());
++
++ if (curr == null) {
++ continue bin_iteration_loop;
++ }
++ }
++
++ TableEntry<K, V> prev;
++
++ /* curr at this point is the bin node */
++
++ for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
++ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
++ if (predicate.test(curr)) {
++ ++removed;
++ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++ prev.setNextRelease(curr = curr.getNextPlain());
++ } else {
++ prev = curr;
++ curr = curr.getNextPlain();
++ }
++ }
++ }
++
++ return removed;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V put(final K key, final V value) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(value, "Null value");
++
++ return this.put(key, value, false);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V putIfAbsent(final K key, final V value) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(value, "Null value");
++
++ return this.put(key, value, true);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean remove(final Object key, final Object value) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(value, "Null value");
++
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int hash = SWMRHashTable.getHash(key);
++ final int index = hash & (table.length - 1);
++
++ final TableEntry<K, V> head = table[index];
++ if (head == null) {
++ return false;
++ }
++
++ if (head.hash == hash && (head.key == key || head.key.equals(key))) {
++ final V currVal = head.getValuePlain();
++
++ if (currVal != value && !currVal.equals(value)) {
++ return false;
++ }
++
++ setAtIndexRelease(table, index, head.getNextPlain());
++ this.removeFromSize(1);
++
++ return true;
++ }
++
++ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++ final V currVal = curr.getValuePlain();
++
++ if (currVal != value && !currVal.equals(value)) {
++ return false;
++ }
++
++ prev.setNextRelease(curr.getNextPlain());
++ this.removeFromSize(1);
++
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ protected final V remove(final Object key, final int hash) {
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int index = (table.length - 1) & hash;
++
++ final TableEntry<K, V> head = table[index];
++ if (head == null) {
++ return null;
++ }
++
++ if (hash == head.hash && (head.key == key || head.key.equals(key))) {
++ setAtIndexRelease(table, index, head.getNextPlain());
++ this.removeFromSize(1);
++
++ return head.getValuePlain();
++ }
++
++ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
++ prev.setNextRelease(curr.getNextPlain());
++ this.removeFromSize(1);
++
++ return curr.getValuePlain();
++ }
++ }
++
++ return null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V remove(final Object key) {
++ Validate.notNull(key, "Null key");
++
++ return this.remove(key, SWMRHashTable.getHash(key));
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean replace(final K key, final V oldValue, final V newValue) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(oldValue, "Null oldValue");
++ Validate.notNull(newValue, "Null newValue");
++
++ final TableEntry<K, V> entry = this.getEntryForPlain(key);
++ if (entry == null) {
++ return false;
++ }
++
++ final V currValue = entry.getValuePlain();
++ if (currValue == oldValue || currValue.equals(oldValue)) {
++ entry.setValueRelease(newValue);
++ return true;
++ }
++
++ return false;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V replace(final K key, final V value) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(value, "Null value");
++
++ final TableEntry<K, V> entry = this.getEntryForPlain(key);
++ if (entry == null) {
++ return null;
++ }
++
++ final V prev = entry.getValuePlain();
++ entry.setValueRelease(value);
++ return prev;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {
++ Validate.notNull(function, "Null function");
++
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<K, V> curr = table[i]; curr != null; curr = curr.getNextPlain()) {
++ final V value = curr.getValuePlain();
++
++ final V newValue = function.apply(curr.key, value);
++ if (newValue == null) {
++ throw new NullPointerException();
++ }
++
++ curr.setValueRelease(newValue);
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public void putAll(final Map<? extends K, ? extends V> map) {
++ Validate.notNull(map, "Null map");
++
++ final int size = map.size();
++ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
++ map.forEach(this::put);
++ }
++
++ /**
++ * {@inheritDoc}
++ * <p>
++ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
++ * is release ordered, that is, after the clear operation is performed a release fence is performed.
++ * </p>
++ */
++ @Override
++ public void clear() {
++ Arrays.fill(this.getTablePlain(), null);
++ this.setSizeRelease(0);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(remappingFunction, "Null remappingFunction");
++
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int index = hash & (table.length - 1);
++
++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++ if (curr == null) {
++ final V newVal = remappingFunction.apply(key ,null);
++
++ if (newVal == null) {
++ return null;
++ }
++
++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
++ if (prev == null) {
++ setAtIndexRelease(table, index, insert);
++ } else {
++ prev.setNextRelease(insert);
++ }
++
++ this.addToSize(1);
++
++ return newVal;
++ }
++
++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++ final V newVal = remappingFunction.apply(key, curr.getValuePlain());
++
++ if (newVal != null) {
++ curr.setValueRelease(newVal);
++ return newVal;
++ }
++
++ if (prev == null) {
++ setAtIndexRelease(table, index, curr.getNextPlain());
++ } else {
++ prev.setNextRelease(curr.getNextPlain());
++ }
++
++ this.removeFromSize(1);
++
++ return null;
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(remappingFunction, "Null remappingFunction");
++
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int index = hash & (table.length - 1);
++
++ for (TableEntry<K, V> curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) {
++ if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) {
++ continue;
++ }
++
++ final V newVal = remappingFunction.apply(key, curr.getValuePlain());
++ if (newVal != null) {
++ curr.setValueRelease(newVal);
++ return newVal;
++ }
++
++ if (prev == null) {
++ setAtIndexRelease(table, index, curr.getNextPlain());
++ } else {
++ prev.setNextRelease(curr.getNextPlain());
++ }
++
++ this.removeFromSize(1);
++
++ return null;
++ }
++
++ return null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(mappingFunction, "Null mappingFunction");
++
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int index = hash & (table.length - 1);
++
++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++ if (curr != null) {
++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++ return curr.getValuePlain();
++ }
++ continue;
++ }
++
++ final V newVal = mappingFunction.apply(key);
++
++ if (newVal == null) {
++ return null;
++ }
++
++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
++ if (prev == null) {
++ setAtIndexRelease(table, index, insert);
++ } else {
++ prev.setNextRelease(insert);
++ }
++
++ this.addToSize(1);
++
++ return newVal;
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
++ Validate.notNull(key, "Null key");
++ Validate.notNull(value, "Null value");
++ Validate.notNull(remappingFunction, "Null remappingFunction");
++
++ final int hash = SWMRHashTable.getHash(key);
++ final TableEntry<K, V>[] table = this.getTablePlain();
++ final int index = hash & (table.length - 1);
++
++ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++ if (curr == null) {
++ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++ if (prev == null) {
++ setAtIndexRelease(table, index, insert);
++ } else {
++ prev.setNextRelease(insert);
++ }
++
++ this.addToSize(1);
++
++ return value;
++ }
++
++ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++ final V newVal = remappingFunction.apply(curr.getValuePlain(), value);
++
++ if (newVal != null) {
++ curr.setValueRelease(newVal);
++ return newVal;
++ }
++
++ if (prev == null) {
++ setAtIndexRelease(table, index, curr.getNextPlain());
++ } else {
++ prev.setNextRelease(curr.getNextPlain());
++ }
++
++ this.removeFromSize(1);
++
++ return null;
++ }
++ }
++ }
++
++ protected static final class TableEntry<K, V> implements Map.Entry<K, V> {
++
++ protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
++
++ protected final int hash;
++ protected final K key;
++ protected V value;
++
++ protected TableEntry<K, V> next;
++
++ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++
++ /* value */
++
++ protected final V getValuePlain() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.get(this);
++ }
++
++ protected final V getValueAcquire() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setValueRelease(final V to) {
++ VALUE_HANDLE.setRelease(this, to);
++ }
++
++ /* next */
++
++ protected final TableEntry<K, V> getNextPlain() {
++ //noinspection unchecked
++ return (TableEntry<K, V>)NEXT_HANDLE.get(this);
++ }
++
++ protected final TableEntry<K, V> getNextOpaque() {
++ //noinspection unchecked
++ return (TableEntry<K, V>)NEXT_HANDLE.getOpaque(this);
++ }
++
++ protected final void setNextPlain(final TableEntry<K, V> next) {
++ NEXT_HANDLE.set(this, next);
++ }
++
++ protected final void setNextRelease(final TableEntry<K, V> next) {
++ NEXT_HANDLE.setRelease(this, next);
++ }
++
++ protected TableEntry(final int hash, final K key, final V value) {
++ this.hash = hash;
++ this.key = key;
++ this.value = value;
++ }
++
++ @Override
++ public K getKey() {
++ return this.key;
++ }
++
++ @Override
++ public V getValue() {
++ return this.getValueAcquire();
++ }
++
++ @Override
++ public V setValue(final V value) {
++ throw new UnsupportedOperationException();
++ }
++
++ protected static int hash(final Object key, final Object value) {
++ return key.hashCode() ^ (value == null ? 0 : value.hashCode());
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public int hashCode() {
++ return hash(this.key, this.getValueAcquire());
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean equals(final Object obj) {
++ if (this == obj) {
++ return true;
++ }
++
++ if (!(obj instanceof Map.Entry<?, ?> other)) {
++ return false;
++ }
++ final Object otherKey = other.getKey();
++ final Object otherValue = other.getValue();
++
++ final K thisKey = this.getKey();
++ final V thisVal = this.getValueAcquire();
++ return (thisKey == otherKey || thisKey.equals(otherKey)) &&
++ (thisVal == otherValue || thisVal.equals(otherValue));
++ }
++ }
++
++
++ protected static abstract class TableEntryIterator<K, V, T> implements Iterator<T> {
++
++ protected final TableEntry<K, V>[] table;
++ protected final SWMRHashTable<K, V> map;
++
++ /* bin which our current element resides on */
++ protected int tableIndex;
++
++ protected TableEntry<K, V> currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */
++ protected TableEntry<K, V> nextEntry; /* may not be on the same bin as currEntry */
++
++ protected TableEntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++ this.table = table;
++ this.map = map;
++ int tableIndex = 0;
++ for (int len = table.length; tableIndex < len; ++tableIndex) {
++ final TableEntry<K, V> entry = getAtIndexOpaque(table, tableIndex);
++ if (entry != null) {
++ this.nextEntry = entry;
++ this.tableIndex = tableIndex + 1;
++ return;
++ }
++ }
++ this.tableIndex = tableIndex;
++ }
++
++ @Override
++ public boolean hasNext() {
++ return this.nextEntry != null;
++ }
++
++ protected final TableEntry<K, V> advanceEntry() {
++ final TableEntry<K, V>[] table = this.table;
++ final int tableLength = table.length;
++ int tableIndex = this.tableIndex;
++ final TableEntry<K, V> curr = this.nextEntry;
++ if (curr == null) {
++ return null;
++ }
++
++ this.currEntry = curr;
++
++ // set up nextEntry
++
++ // find next in chain
++ TableEntry<K, V> next = curr.getNextOpaque();
++
++ if (next != null) {
++ this.nextEntry = next;
++ return curr;
++ }
++
++ // nothing in chain, so find next available bin
++ for (;tableIndex < tableLength; ++tableIndex) {
++ next = getAtIndexOpaque(table, tableIndex);
++ if (next != null) {
++ this.nextEntry = next;
++ this.tableIndex = tableIndex + 1;
++ return curr;
++ }
++ }
++
++ this.nextEntry = null;
++ this.tableIndex = tableIndex;
++ return curr;
++ }
++
++ @Override
++ public void remove() {
++ final TableEntry<K, V> curr = this.currEntry;
++ if (curr == null) {
++ throw new IllegalStateException();
++ }
++
++ this.map.remove(curr.key, curr.hash);
++
++ this.currEntry = null;
++ }
++ }
++
++ protected static final class ValueIterator<K, V> extends TableEntryIterator<K, V, V> {
++
++ protected ValueIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++ super(table, map);
++ }
++
++ @Override
++ public V next() {
++ final TableEntry<K, V> entry = this.advanceEntry();
++
++ if (entry == null) {
++ throw new NoSuchElementException();
++ }
++
++ return entry.getValueAcquire();
++ }
++ }
++
++ protected static final class KeyIterator<K, V> extends TableEntryIterator<K, V, K> {
++
++ protected KeyIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++ super(table, map);
++ }
++
++ @Override
++ public K next() {
++ final TableEntry<K, V> curr = this.advanceEntry();
++
++ if (curr == null) {
++ throw new NoSuchElementException();
++ }
++
++ return curr.key;
++ }
++ }
++
++ protected static final class EntryIterator<K, V> extends TableEntryIterator<K, V, Map.Entry<K, V>> {
++
++ protected EntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++ super(table, map);
++ }
++
++ @Override
++ public Map.Entry<K, V> next() {
++ final TableEntry<K, V> curr = this.advanceEntry();
++
++ if (curr == null) {
++ throw new NoSuchElementException();
++ }
++
++ return curr;
++ }
++ }
++
++ protected static abstract class ViewCollection<K, V, T> implements Collection<T> {
++
++ protected final SWMRHashTable<K, V> map;
++
++ protected ViewCollection(final SWMRHashTable<K, V> map) {
++ this.map = map;
++ }
++
++ @Override
++ public boolean add(final T element) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean addAll(final Collection<? extends T> collections) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean removeAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ boolean modified = false;
++ for (final Object element : collection) {
++ modified |= this.remove(element);
++ }
++ return modified;
++ }
++
++ @Override
++ public int size() {
++ return this.map.size();
++ }
++
++ @Override
++ public boolean isEmpty() {
++ return this.size() == 0;
++ }
++
++ @Override
++ public void clear() {
++ this.map.clear();
++ }
++
++ @Override
++ public boolean containsAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ for (final Object element : collection) {
++ if (!this.contains(element)) {
++ return false;
++ }
++ }
++
++ return true;
++ }
++
++ @Override
++ public Object[] toArray() {
++ final List<T> list = new ArrayList<>(this.size());
++
++ this.forEach(list::add);
++
++ return list.toArray();
++ }
++
++ @Override
++ public <E> E[] toArray(final E[] array) {
++ final List<T> list = new ArrayList<>(this.size());
++
++ this.forEach(list::add);
++
++ return list.toArray(array);
++ }
++
++ @Override
++ public <E> E[] toArray(final IntFunction<E[]> generator) {
++ final List<T> list = new ArrayList<>(this.size());
++
++ this.forEach(list::add);
++
++ return list.toArray(generator);
++ }
++
++ @Override
++ public int hashCode() {
++ int hash = 0;
++ for (final T element : this) {
++ hash += element == null ? 0 : element.hashCode();
++ }
++ return hash;
++ }
++
++ @Override
++ public Spliterator<T> spliterator() { // TODO implement
++ return Spliterators.spliterator(this, Spliterator.NONNULL);
++ }
++ }
++
++ protected static abstract class ViewSet<K, V, T> extends ViewCollection<K, V, T> implements Set<T> {
++
++ protected ViewSet(final SWMRHashTable<K, V> map) {
++ super(map);
++ }
++
++ @Override
++ public boolean equals(final Object obj) {
++ if (this == obj) {
++ return true;
++ }
++
++ if (!(obj instanceof Set)) {
++ return false;
++ }
++
++ final Set<?> other = (Set<?>)obj;
++ if (other.size() != this.size()) {
++ return false;
++ }
++
++ return this.containsAll(other);
++ }
++ }
++
++ protected static final class EntrySet<K, V> extends ViewSet<K, V, Map.Entry<K, V>> implements Set<Map.Entry<K, V>> {
++
++ protected EntrySet(final SWMRHashTable<K, V> map) {
++ super(map);
++ }
++
++ @Override
++ public boolean remove(final Object object) {
++ if (!(object instanceof Map.Entry<?, ?> entry)) {
++ return false;
++ }
++
++ final Object key;
++ final Object value;
++
++ try {
++ key = entry.getKey();
++ value = entry.getValue();
++ } catch (final IllegalStateException ex) {
++ return false;
++ }
++
++ return this.map.remove(key, value);
++ }
++
++ @Override
++ public boolean removeIf(final Predicate<? super Map.Entry<K, V>> filter) {
++ Validate.notNull(filter, "Null filter");
++
++ return this.map.removeEntryIf(filter) != 0;
++ }
++
++ @Override
++ public boolean retainAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ return this.map.removeEntryIf((final Map.Entry<K, V> entry) -> {
++ return !collection.contains(entry);
++ }) != 0;
++ }
++
++ @Override
++ public Iterator<Map.Entry<K, V>> iterator() {
++ return new EntryIterator<>(this.map.getTableAcquire(), this.map);
++ }
++
++ @Override
++ public void forEach(final Consumer<? super Map.Entry<K, V>> action) {
++ this.map.forEach(action);
++ }
++
++ @Override
++ public boolean contains(final Object object) {
++ if (!(object instanceof Map.Entry<?, ?> entry)) {
++ return false;
++ }
++
++ final Object key;
++ final Object value;
++
++ try {
++ key = entry.getKey();
++ value = entry.getValue();
++ } catch (final IllegalStateException ex) {
++ return false;
++ }
++
++ return this.map.contains(key, value);
++ }
++
++ @Override
++ public String toString() {
++ return CollectionUtil.toString(this, "SWMRHashTableEntrySet");
++ }
++ }
++
++ protected static final class KeySet<K, V> extends ViewSet<K, V, K> {
++
++ protected KeySet(final SWMRHashTable<K, V> map) {
++ super(map);
++ }
++
++ @Override
++ public Iterator<K> iterator() {
++ return new KeyIterator<>(this.map.getTableAcquire(), this.map);
++ }
++
++ @Override
++ public void forEach(final Consumer<? super K> action) {
++ Validate.notNull(action, "Null action");
++
++ this.map.forEachKey(action);
++ }
++
++ @Override
++ public boolean contains(final Object key) {
++ Validate.notNull(key, "Null key");
++
++ return this.map.containsKey(key);
++ }
++
++ @Override
++ public boolean remove(final Object key) {
++ Validate.notNull(key, "Null key");
++
++ return this.map.remove(key) != null;
++ }
++
++ @Override
++ public boolean retainAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ return this.map.removeIf((final K key, final V value) -> {
++ return !collection.contains(key);
++ }) != 0;
++ }
++
++ @Override
++ public boolean removeIf(final Predicate<? super K> filter) {
++ Validate.notNull(filter, "Null filter");
++
++ return this.map.removeIf((final K key, final V value) -> {
++ return filter.test(key);
++ }) != 0;
++ }
++
++ @Override
++ public String toString() {
++ return CollectionUtil.toString(this, "SWMRHashTableKeySet");
++ }
++ }
++
++ protected static final class ValueCollection<K, V> extends ViewSet<K, V, V> implements Collection<V> {
++
++ protected ValueCollection(final SWMRHashTable<K, V> map) {
++ super(map);
++ }
++
++ @Override
++ public Iterator<V> iterator() {
++ return new ValueIterator<>(this.map.getTableAcquire(), this.map);
++ }
++
++ @Override
++ public void forEach(final Consumer<? super V> action) {
++ Validate.notNull(action, "Null action");
++
++ this.map.forEachValue(action);
++ }
++
++ @Override
++ public boolean contains(final Object object) {
++ Validate.notNull(object, "Null object");
++
++ return this.map.containsValue(object);
++ }
++
++ @Override
++ public boolean remove(final Object object) {
++ Validate.notNull(object, "Null object");
++
++ final Iterator<V> itr = this.iterator();
++ while (itr.hasNext()) {
++ final V val = itr.next();
++ if (val == object || val.equals(object)) {
++ itr.remove();
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean removeIf(final Predicate<? super V> filter) {
++ Validate.notNull(filter, "Null filter");
++
++ return this.map.removeIf((final K key, final V value) -> {
++ return filter.test(value);
++ }) != 0;
++ }
++
++ @Override
++ public boolean retainAll(final Collection<?> collection) {
++ Validate.notNull(collection, "Null collection");
++
++ return this.map.removeIf((final K key, final V value) -> {
++ return !collection.contains(value);
++ }) != 0;
++ }
++
++ @Override
++ public String toString() {
++ return CollectionUtil.toString(this, "SWMRHashTableValues");
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bb301a9f4e3ac919552eef68afc73569d50674db
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
+@@ -0,0 +1,674 @@
++package ca.spottedleaf.concurrentutil.map;
++
++import ca.spottedleaf.concurrentutil.function.BiLongObjectConsumer;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.HashUtil;
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import java.lang.invoke.VarHandle;
++import java.util.Arrays;
++import java.util.function.Consumer;
++import java.util.function.LongConsumer;
++
++// trimmed down version of SWMRHashTable
++public class SWMRLong2ObjectHashTable<V> {
++
++ protected int size;
++
++ protected TableEntry<V>[] table;
++
++ protected final float loadFactor;
++
++ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class);
++ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class);
++
++ /* size */
++
++ protected final int getSizePlain() {
++ return (int)SIZE_HANDLE.get(this);
++ }
++
++ protected final int getSizeOpaque() {
++ return (int)SIZE_HANDLE.getOpaque(this);
++ }
++
++ protected final int getSizeAcquire() {
++ return (int)SIZE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setSizePlain(final int value) {
++ SIZE_HANDLE.set(this, value);
++ }
++
++ protected final void setSizeOpaque(final int value) {
++ SIZE_HANDLE.setOpaque(this, value);
++ }
++
++ protected final void setSizeRelease(final int value) {
++ SIZE_HANDLE.setRelease(this, value);
++ }
++
++ /* table */
++
++ protected final TableEntry<V>[] getTablePlain() {
++ //noinspection unchecked
++ return (TableEntry<V>[])TABLE_HANDLE.get(this);
++ }
++
++ protected final TableEntry<V>[] getTableAcquire() {
++ //noinspection unchecked
++ return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setTablePlain(final TableEntry<V>[] table) {
++ TABLE_HANDLE.set(this, table);
++ }
++
++ protected final void setTableRelease(final TableEntry<V>[] table) {
++ TABLE_HANDLE.setRelease(this, table);
++ }
++
++ protected static final int DEFAULT_CAPACITY = 16;
++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
++
++ /**
++ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
++ */
++ public SWMRLong2ObjectHashTable() {
++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
++ }
++
++ /**
++ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
++ * @param capacity specified initial capacity, > 0
++ */
++ public SWMRLong2ObjectHashTable(final int capacity) {
++ this(capacity, DEFAULT_LOAD_FACTOR);
++ }
++
++ /**
++ * Constructs this map with the specified capacity and load factor.
++ * @param capacity specified capacity, > 0
++ * @param loadFactor specified load factor, > 0 && finite
++ */
++ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) {
++ final int tableSize = getCapacityFor(capacity);
++
++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
++ }
++
++ //noinspection unchecked
++ final TableEntry<V>[] table = new TableEntry[tableSize];
++ this.setTablePlain(table);
++
++ if (tableSize == MAXIMUM_CAPACITY) {
++ this.threshold = -1;
++ } else {
++ this.threshold = getTargetCapacity(tableSize, loadFactor);
++ }
++
++ this.loadFactor = loadFactor;
++ }
++
++ /**
++ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
++ * with a load factor of {@code 0.75f}.
++ * All of the specified map's entries are copied into this map.
++ * @param other The specified map.
++ */
++ public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable<V> other) {
++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
++ }
++
++ /**
++ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
++ * with a load factor of {@code 0.75f}.
++ * All of the specified map's entries are copied into this map.
++ * @param capacity specified capacity, > 0
++ * @param other The specified map.
++ */
++ public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable<V> other) {
++ this(capacity, DEFAULT_LOAD_FACTOR, other);
++ }
++
++ /**
++ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
++ * with the specified load factor.
++ * All of the specified map's entries are copied into this map.
++ * @param capacity specified capacity, > 0
++ * @param loadFactor specified load factor, > 0 && finite
++ * @param other The specified map.
++ */
++ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) {
++ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
++ this.putAll(other);
++ }
++
++ protected static <V> TableEntry<V> getAtIndexOpaque(final TableEntry<V>[] table, final int index) {
++ // noinspection unchecked
++ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
++ }
++
++ protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
++ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
++ }
++
++ public final float getLoadFactor() {
++ return this.loadFactor;
++ }
++
++ protected static int getCapacityFor(final int capacity) {
++ if (capacity <= 0) {
++ throw new IllegalArgumentException("Invalid capacity: " + capacity);
++ }
++ if (capacity >= MAXIMUM_CAPACITY) {
++ return MAXIMUM_CAPACITY;
++ }
++ return IntegerUtil.roundCeilLog2(capacity);
++ }
++
++ /** Callers must still use acquire when reading the value of the entry. */
++ protected final TableEntry<V> getEntryForOpaque(final long key) {
++ final int hash = SWMRLong2ObjectHashTable.getHash(key);
++ final TableEntry<V>[] table = this.getTableAcquire();
++
++ for (TableEntry<V> curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++ if (key == curr.key) {
++ return curr;
++ }
++ }
++
++ return null;
++ }
++
++ protected final TableEntry<V> getEntryForPlain(final long key) {
++ final int hash = SWMRLong2ObjectHashTable.getHash(key);
++ final TableEntry<V>[] table = this.getTablePlain();
++
++ for (TableEntry<V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
++ if (key == curr.key) {
++ return curr;
++ }
++ }
++
++ return null;
++ }
++
++ /* MT-Safe */
++
++ /** must be deterministic given a key */
++ protected static int getHash(final long key) {
++ return (int)HashUtil.mix(key);
++ }
++
++ // rets -1 if capacity*loadFactor is too large
++ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
++ final double ret = (double)capacity * (double)loadFactor;
++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
++ return -1;
++ }
++
++ return (int)ret;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public boolean equals(final Object obj) {
++ if (this == obj) {
++ return true;
++ }
++ /* Make no attempt to deal with concurrent modifications */
++ if (!(obj instanceof SWMRLong2ObjectHashTable<?> other)) {
++ return false;
++ }
++
++ if (this.size() != other.size()) {
++ return false;
++ }
++
++ final TableEntry<V>[] table = this.getTableAcquire();
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ final Object otherValue = other.get(curr.key);
++ if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public int hashCode() {
++ /* Make no attempt to deal with concurrent modifications */
++ int hash = 0;
++ final TableEntry<V>[] table = this.getTableAcquire();
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ hash += curr.hashCode();
++ }
++ }
++
++ return hash;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public String toString() {
++ final StringBuilder builder = new StringBuilder(64);
++ builder.append("SingleWriterMultiReaderHashMap:{");
++
++ this.forEach((final long key, final V value) -> {
++ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
++ });
++
++ return builder.append('}').toString();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public SWMRLong2ObjectHashTable<V> clone() {
++ return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public void forEach(final Consumer<? super TableEntry<V>> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ action.accept(curr);
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public void forEach(final BiLongObjectConsumer<? super V> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ action.accept(curr.key, value);
++ }
++ }
++ }
++
++ /**
++ * Provides the specified consumer with all keys contained within this map.
++ * @param action The specified consumer.
++ */
++ public void forEachKey(final LongConsumer action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ action.accept(curr.key);
++ }
++ }
++ }
++
++ /**
++ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
++ * @param action The specified consumer.
++ */
++ public void forEachValue(final Consumer<? super V> action) {
++ Validate.notNull(action, "Null action");
++
++ final TableEntry<V>[] table = this.getTableAcquire();
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++ final V value = curr.getValueAcquire();
++
++ action.accept(value);
++ }
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public V get(final long key) {
++ final TableEntry<V> entry = this.getEntryForOpaque(key);
++ return entry == null ? null : entry.getValueAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public boolean containsKey(final long key) {
++ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
++ return this.get(key) != null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public V getOrDefault(final long key, final V defaultValue) {
++ final TableEntry<V> entry = this.getEntryForOpaque(key);
++
++ return entry == null ? defaultValue : entry.getValueAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public int size() {
++ return this.getSizeAcquire();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public boolean isEmpty() {
++ return this.getSizeAcquire() == 0;
++ }
++
++ /* Non-MT-Safe */
++
++ protected int threshold;
++
++ protected final void checkResize(final int minCapacity) {
++ if (minCapacity <= this.threshold || this.threshold < 0) {
++ return;
++ }
++
++ final TableEntry<V>[] table = this.getTablePlain();
++ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
++ if (newCapacity < 0) {
++ newCapacity = MAXIMUM_CAPACITY;
++ }
++ if (newCapacity <= table.length) {
++ if (newCapacity == MAXIMUM_CAPACITY) {
++ return;
++ }
++ newCapacity = table.length << 1;
++ }
++
++ //noinspection unchecked
++ final TableEntry<V>[] newTable = new TableEntry[newCapacity];
++ final int indexMask = newCapacity - 1;
++
++ for (int i = 0, len = table.length; i < len; ++i) {
++ for (TableEntry<V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
++ final long key = entry.key;
++ final int hash = SWMRLong2ObjectHashTable.getHash(key);
++ final int index = hash & indexMask;
++
++ /* we need to create a new entry since there could be reading threads */
++ final TableEntry<V> insert = new TableEntry<>(key, entry.getValuePlain());
++
++ final TableEntry<V> prev = newTable[index];
++
++ newTable[index] = insert;
++ insert.setNextPlain(prev);
++ }
++ }
++
++ if (newCapacity == MAXIMUM_CAPACITY) {
++ this.threshold = -1; /* No more resizing */
++ } else {
++ this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
++ }
++ this.setTableRelease(newTable); /* use release to publish entries in table */
++ }
++
++ protected final int addToSize(final int num) {
++ final int newSize = this.getSizePlain() + num;
++
++ this.setSizeOpaque(newSize);
++ this.checkResize(newSize);
++
++ return newSize;
++ }
++
++ protected final int removeFromSize(final int num) {
++ final int newSize = this.getSizePlain() - num;
++
++ this.setSizeOpaque(newSize);
++
++ return newSize;
++ }
++
++ protected final V put(final long key, final V value, final boolean onlyIfAbsent) {
++ final TableEntry<V>[] table = this.getTablePlain();
++ final int hash = SWMRLong2ObjectHashTable.getHash(key);
++ final int index = hash & (table.length - 1);
++
++ final TableEntry<V> head = table[index];
++ if (head == null) {
++ final TableEntry<V> insert = new TableEntry<>(key, value);
++ setAtIndexRelease(table, index, insert);
++ this.addToSize(1);
++ return null;
++ }
++
++ for (TableEntry<V> curr = head;;) {
++ if (key == curr.key) {
++ if (onlyIfAbsent) {
++ return curr.getValuePlain();
++ }
++
++ final V currVal = curr.getValuePlain();
++ curr.setValueRelease(value);
++ return currVal;
++ }
++
++ final TableEntry<V> next = curr.getNextPlain();
++ if (next != null) {
++ curr = next;
++ continue;
++ }
++
++ final TableEntry<V> insert = new TableEntry<>(key, value);
++
++ curr.setNextRelease(insert);
++ this.addToSize(1);
++ return null;
++ }
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public V put(final long key, final V value) {
++ Validate.notNull(value, "Null value");
++
++ return this.put(key, value, false);
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public V putIfAbsent(final long key, final V value) {
++ Validate.notNull(value, "Null value");
++
++ return this.put(key, value, true);
++ }
++
++ protected final V remove(final long key, final int hash) {
++ final TableEntry<V>[] table = this.getTablePlain();
++ final int index = (table.length - 1) & hash;
++
++ final TableEntry<V> head = table[index];
++ if (head == null) {
++ return null;
++ }
++
++ if (head.key == key) {
++ setAtIndexRelease(table, index, head.getNextPlain());
++ this.removeFromSize(1);
++
++ return head.getValuePlain();
++ }
++
++ for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++ if (key == curr.key) {
++ prev.setNextRelease(curr.getNextPlain());
++ this.removeFromSize(1);
++
++ return curr.getValuePlain();
++ }
++ }
++
++ return null;
++ }
++
++ protected final V remove(final long key, final int hash, final V expect) {
++ final TableEntry<V>[] table = this.getTablePlain();
++ final int index = (table.length - 1) & hash;
++
++ final TableEntry<V> head = table[index];
++ if (head == null) {
++ return null;
++ }
++
++ if (head.key == key) {
++ final V val = head.value;
++ if (val == expect || val.equals(expect)) {
++ setAtIndexRelease(table, index, head.getNextPlain());
++ this.removeFromSize(1);
++
++ return head.getValuePlain();
++ } else {
++ return null;
++ }
++ }
++
++ for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++ if (key == curr.key) {
++ final V val = curr.value;
++ if (val == expect || val.equals(expect)) {
++ prev.setNextRelease(curr.getNextPlain());
++ this.removeFromSize(1);
++
++ return curr.getValuePlain();
++ } else {
++ return null;
++ }
++ }
++ }
++
++ return null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public V remove(final long key) {
++ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
++ }
++
++ public boolean remove(final long key, final V expect) {
++ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ public void putAll(final SWMRLong2ObjectHashTable<? extends V> map) {
++ Validate.notNull(map, "Null map");
++
++ final int size = map.size();
++ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
++ map.forEach(this::put);
++ }
++
++ /**
++ * {@inheritDoc}
++ * <p>
++ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
++ * is release ordered, that is, after the clear operation is performed a release fence is performed.
++ * </p>
++ */
++ public void clear() {
++ Arrays.fill(this.getTablePlain(), null);
++ this.setSizeRelease(0);
++ }
++
++ public static final class TableEntry<V> {
++
++ protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
++
++ protected final long key;
++ protected V value;
++
++ protected TableEntry<V> next;
++
++ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++
++ /* value */
++
++ protected final V getValuePlain() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.get(this);
++ }
++
++ protected final V getValueAcquire() {
++ //noinspection unchecked
++ return (V)VALUE_HANDLE.getAcquire(this);
++ }
++
++ protected final void setValueRelease(final V to) {
++ VALUE_HANDLE.setRelease(this, to);
++ }
++
++ /* next */
++
++ protected final TableEntry<V> getNextPlain() {
++ //noinspection unchecked
++ return (TableEntry<V>)NEXT_HANDLE.get(this);
++ }
++
++ protected final TableEntry<V> getNextOpaque() {
++ //noinspection unchecked
++ return (TableEntry<V>)NEXT_HANDLE.getOpaque(this);
++ }
++
++ protected final void setNextPlain(final TableEntry<V> next) {
++ NEXT_HANDLE.set(this, next);
++ }
++
++ protected final void setNextRelease(final TableEntry<V> next) {
++ NEXT_HANDLE.setRelease(this, next);
++ }
++
++ protected TableEntry(final long key, final V value) {
++ this.key = key;
++ this.value = value;
++ }
++
++ public long getKey() {
++ return this.key;
++ }
++
++ public V getValue() {
++ return this.getValueAcquire();
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..85e6ef636d435a0ee4bf3e0760b0c87422c520a1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
+@@ -0,0 +1,564 @@
++package ca.spottedleaf.concurrentutil.scheduler;
++
++import ca.spottedleaf.concurrentutil.set.LinkedSortedSet;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import java.lang.invoke.VarHandle;
++import java.util.BitSet;
++import java.util.Comparator;
++import java.util.PriorityQueue;
++import java.util.concurrent.ThreadFactory;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.concurrent.locks.LockSupport;
++import java.util.function.BooleanSupplier;
++
++/**
++ * @deprecated To be replaced
++ */
++@Deprecated
++public class SchedulerThreadPool {
++
++ public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
++
++ private static final Comparator<SchedulableTick> TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> {
++ final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart);
++ if (timeCompare != 0) {
++ return timeCompare;
++ }
++
++ return Long.compare(t1.id, t2.id);
++ };
++
++ private final TickThreadRunner[] runners;
++ private final Thread[] threads;
++ private final LinkedSortedSet<SchedulableTick> awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
++ private final PriorityQueue<SchedulableTick> queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME);
++ private final BitSet idleThreads;
++
++ private final Object scheduleLock = new Object();
++
++ private volatile boolean halted;
++
++ /**
++ * Creates, but does not start, a scheduler thread pool with the specified number of threads
++ * created using the specified thread factory.
++ * @param threads Specified number of threads
++ * @param threadFactory Specified thread factory
++ * @see #start()
++ */
++ public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) {
++ final BitSet idleThreads = new BitSet(threads);
++ for (int i = 0; i < threads; ++i) {
++ idleThreads.set(i);
++ }
++ this.idleThreads = idleThreads;
++
++ final TickThreadRunner[] runners = new TickThreadRunner[threads];
++ final Thread[] t = new Thread[threads];
++ for (int i = 0; i < threads; ++i) {
++ runners[i] = new TickThreadRunner(i, this);
++ t[i] = threadFactory.newThread(runners[i]);
++ }
++
++ this.threads = t;
++ this.runners = runners;
++ }
++
++ /**
++ * Starts the threads in this pool.
++ */
++ public void start() {
++ for (final Thread thread : this.threads) {
++ thread.start();
++ }
++ }
++
++ /**
++ * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die.
++ *
++ * @param sync Whether to wait for the scheduler threads to die.
++ * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die.
++ * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout.
++ * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time.
++ */
++ public boolean halt(final boolean sync, final long maxWaitNS) {
++ this.halted = true;
++ for (final Thread thread : this.threads) {
++ // force response to halt
++ LockSupport.unpark(thread);
++ }
++ final long time = System.nanoTime();
++ if (sync) {
++ // start at 10 * 0.5ms -> 5ms
++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
++ boolean allDead = true;
++ for (final Thread thread : this.threads) {
++ if (thread.isAlive()) {
++ allDead = false;
++ break;
++ }
++ }
++ if (allDead) {
++ return true;
++ }
++ if ((System.nanoTime() - time) >= maxWaitNS) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ /**
++ * Returns an array of the underlying scheduling threads.
++ */
++ public Thread[] getThreads() {
++ return this.threads.clone();
++ }
++
++ private void insertFresh(final SchedulableTick task) {
++ final TickThreadRunner[] runners = this.runners;
++
++ final int firstIdleThread = this.idleThreads.nextSetBit(0);
++
++ if (firstIdleThread != -1) {
++ // push to idle thread
++ this.idleThreads.clear(firstIdleThread);
++ final TickThreadRunner runner = runners[firstIdleThread];
++ task.awaitingLink = this.awaiting.addLast(task);
++ runner.acceptTask(task);
++ return;
++ }
++
++ // try to replace the last awaiting task
++ final SchedulableTick last = this.awaiting.last();
++
++ if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) {
++ // need to replace the last task
++ this.awaiting.pollLast();
++ last.awaitingLink = null;
++ task.awaitingLink = this.awaiting.addLast(task);
++ // need to add task to queue to be picked up later
++ this.queued.add(last);
++
++ final TickThreadRunner runner = last.ownedBy;
++ runner.replaceTask(task);
++
++ return;
++ }
++
++ // add to queue, will be picked up later
++ this.queued.add(task);
++ }
++
++ private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) {
++ if (!this.awaiting.remove(tick.awaitingLink)) {
++ throw new IllegalStateException("Task is not in awaiting");
++ }
++ tick.awaitingLink = null;
++ }
++
++ private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) {
++ if (reschedule != null) {
++ this.queued.add(reschedule);
++ }
++ final SchedulableTick ret = this.queued.poll();
++ if (ret == null) {
++ this.idleThreads.set(runner.id);
++ } else {
++ ret.awaitingLink = this.awaiting.addLast(ret);
++ }
++
++ return ret;
++ }
++
++ /**
++ * Schedules the specified task to be executed on this thread pool.
++ * @param task Specified task
++ * @throws IllegalStateException If the task is already scheduled
++ * @see SchedulableTick
++ */
++ public void schedule(final SchedulableTick task) {
++ synchronized (this.scheduleLock) {
++ if (!task.tryMarkScheduled()) {
++ throw new IllegalStateException("Task " + task + " is already scheduled or cancelled");
++ }
++
++ task.schedulerOwnedBy = this;
++
++ this.insertFresh(task);
++ }
++ }
++
++ /**
++ * Updates the tasks scheduled start to the maximum of its current scheduled start and the specified
++ * new start. If the task is not scheduled, returns {@code false}. Otherwise, returns whether the
++ * scheduled start was updated. Undefined behavior of the specified task is scheduled in another executor.
++ * @param task Specified task
++ * @param newStart Specified new start
++ */
++ public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) {
++ synchronized (this.scheduleLock) {
++ if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) {
++ return false;
++ }
++ if (this.queued.remove(task)) {
++ task.setScheduledStart(newStart);
++ this.queued.add(task);
++ return true;
++ }
++ if (task.awaitingLink != null) {
++ this.awaiting.remove(task.awaitingLink);
++ task.awaitingLink = null;
++
++ // re-queue task
++ task.setScheduledStart(newStart);
++ this.queued.add(task);
++
++ // now we need to replace the task the runner was waiting for
++ final TickThreadRunner runner = task.ownedBy;
++ final SchedulableTick replace = this.queued.poll();
++
++ // replace cannot be null, since we have added a task to queued
++ if (replace != task) {
++ runner.replaceTask(replace);
++ }
++
++ return true;
++ }
++
++ return false;
++ }
++ }
++
++ /**
++ * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled
++ * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing.
++ */
++ public Boolean tryRetire(final SchedulableTick task) {
++ if (task.schedulerOwnedBy != this) {
++ return null;
++ }
++
++ synchronized (this.scheduleLock) {
++ if (this.queued.remove(task)) {
++ // cancelled, and no runner owns it - so return
++ return Boolean.TRUE;
++ }
++ if (task.awaitingLink != null) {
++ this.awaiting.remove(task.awaitingLink);
++ task.awaitingLink = null;
++ // here we need to replace the task the runner was waiting for
++ final TickThreadRunner runner = task.ownedBy;
++ final SchedulableTick replace = this.queued.poll();
++
++ if (replace == null) {
++ // nothing to replace with, set to idle
++ this.idleThreads.set(runner.id);
++ runner.forceIdle();
++ } else {
++ runner.replaceTask(replace);
++ }
++
++ return Boolean.TRUE;
++ }
++
++ // could not find it in queue
++ return task.tryMarkCancelled() ? Boolean.FALSE : null;
++ }
++ }
++
++ /**
++ * Indicates that intermediate tasks are available to be executed by the task.
++ * <p>
++ * Note: currently a no-op
++ * </p>
++ * @param task The specified task
++ * @see SchedulableTick
++ */
++ public void notifyTasks(final SchedulableTick task) {
++ // Not implemented
++ }
++
++ /**
++ * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}.
++ * <p>
++ * A tickable task is expected to run on a fixed interval, which is determined by
++ * the {@link SchedulerThreadPool}.
++ * </p>
++ * <p>
++ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of
++ * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain
++ * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take
++ * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking.
++ * </p>
++ * <p>
++ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never
++ * invoked in parallel.
++ * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)}
++ * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to
++ * parse intermediate tasks.
++ * </p>
++ * @deprecated To be replaced
++ */
++ @Deprecated
++ public static abstract class SchedulableTick {
++ private static final AtomicLong ID_GENERATOR = new AtomicLong();
++ public final long id = ID_GENERATOR.getAndIncrement();
++
++ private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0;
++ private static final int SCHEDULE_STATE_SCHEDULED = 1;
++ private static final int SCHEDULE_STATE_CANCELLED = 2;
++
++ private final AtomicInteger scheduled = new AtomicInteger();
++ private SchedulerThreadPool schedulerOwnedBy;
++ private long scheduledStart = DEADLINE_NOT_SET;
++ private TickThreadRunner ownedBy;
++
++ private LinkedSortedSet.Link<SchedulableTick> awaitingLink;
++
++ private boolean tryMarkScheduled() {
++ return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED);
++ }
++
++ private boolean tryMarkCancelled() {
++ return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED);
++ }
++
++ private boolean isScheduled() {
++ return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED;
++ }
++
++ protected final long getScheduledStart() {
++ return this.scheduledStart;
++ }
++
++ /**
++ * If this task is scheduled, then this may only be invoked during {@link #runTick()},
++ * and {@link #runTasks(BooleanSupplier)}
++ */
++ protected final void setScheduledStart(final long value) {
++ this.scheduledStart = value;
++ }
++
++ /**
++ * Executes the tick.
++ * <p>
++ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
++ * the next tick.
++ * </p>
++ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise.
++ */
++ public abstract boolean runTick();
++
++ /**
++ * Returns whether this task has any intermediate tasks that can be executed.
++ */
++ public abstract boolean hasTasks();
++
++ /**
++ * Returns {@code null} if this task should not be scheduled, otherwise returns
++ * {@code Boolean.TRUE} if there are more intermediate tasks to execute and
++ * {@code Boolean.FALSE} if there are no more intermediate tasks to execute.
++ */
++ public abstract Boolean runTasks(final BooleanSupplier canContinue);
++
++ @Override
++ public String toString() {
++ return "SchedulableTick:{" +
++ "class=" + this.getClass().getName() + "," +
++ "scheduled_state=" + this.scheduled.get() + ","
++ + "}";
++ }
++ }
++
++ private static final class TickThreadRunner implements Runnable {
++
++ /**
++ * There are no tasks in this thread's runqueue, so it is parked.
++ * <p>
++ * stateTarget = null
++ * </p>
++ */
++ private static final int STATE_IDLE = 0;
++
++ /**
++ * The runner is waiting to tick a task, as it has no intermediate tasks to execute.
++ * <p>
++ * stateTarget = the task awaiting tick
++ * </p>
++ */
++ private static final int STATE_AWAITING_TICK = 1;
++
++ /**
++ * The runner is executing a tick for one of the tasks that was in its runqueue.
++ * <p>
++ * stateTarget = the task being ticked
++ * </p>
++ */
++ private static final int STATE_EXECUTING_TICK = 2;
++
++ public final int id;
++ public final SchedulerThreadPool scheduler;
++
++ private volatile Thread thread;
++ private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE);
++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class);
++
++ private void setStatePlain(final TickThreadRunnerState state) {
++ STATE_HANDLE.set(this, state);
++ }
++
++ private void setStateOpaque(final TickThreadRunnerState state) {
++ STATE_HANDLE.setOpaque(this, state);
++ }
++
++ private void setStateVolatile(final TickThreadRunnerState state) {
++ STATE_HANDLE.setVolatile(this, state);
++ }
++
++ private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {}
++
++ public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) {
++ this.id = id;
++ this.scheduler = scheduler;
++ }
++
++ private Thread getRunnerThread() {
++ return this.thread;
++ }
++
++ private void acceptTask(final SchedulableTick task) {
++ if (task.ownedBy != null) {
++ throw new IllegalStateException("Already owned by another runner");
++ }
++ task.ownedBy = this;
++ final TickThreadRunnerState state = this.state;
++ if (state.state != STATE_IDLE) {
++ throw new IllegalStateException("Cannot accept task in state " + state);
++ }
++ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
++ LockSupport.unpark(this.getRunnerThread());
++ }
++
++ private void replaceTask(final SchedulableTick task) {
++ final TickThreadRunnerState state = this.state;
++ if (state.state != STATE_AWAITING_TICK) {
++ throw new IllegalStateException("Cannot replace task in state " + state);
++ }
++ if (task.ownedBy != null) {
++ throw new IllegalStateException("Already owned by another runner");
++ }
++ task.ownedBy = this;
++
++ state.stateTarget.ownedBy = null;
++
++ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
++ LockSupport.unpark(this.getRunnerThread());
++ }
++
++ private void forceIdle() {
++ final TickThreadRunnerState state = this.state;
++ if (state.state != STATE_AWAITING_TICK) {
++ throw new IllegalStateException("Cannot replace task in state " + state);
++ }
++ state.stateTarget.ownedBy = null;
++ this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE));
++ // no need to unpark
++ }
++
++ private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) {
++ synchronized (this.scheduler.scheduleLock) {
++ if (this.state != state) {
++ return false;
++ }
++ this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK));
++ this.scheduler.takeTask(this, task);
++ return true;
++ }
++ }
++
++ private void returnTask(final SchedulableTick task, final boolean reschedule) {
++ synchronized (this.scheduler.scheduleLock) {
++ task.ownedBy = null;
++
++ final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null);
++ if (newWait == null) {
++ this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE));
++ } else {
++ if (newWait.ownedBy != null) {
++ throw new IllegalStateException("Already owned by another runner");
++ }
++ newWait.ownedBy = this;
++ this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK));
++ }
++ }
++ }
++
++ @Override
++ public void run() {
++ this.thread = Thread.currentThread();
++
++ main_state_loop:
++ for (;;) {
++ final TickThreadRunnerState startState = this.state;
++ final int startStateType = startState.state;
++ final SchedulableTick startStateTask = startState.stateTarget;
++
++ if (this.scheduler.halted) {
++ return;
++ }
++
++ switch (startStateType) {
++ case STATE_IDLE: {
++ while (this.state.state == STATE_IDLE) {
++ LockSupport.park();
++ if (this.scheduler.halted) {
++ return;
++ }
++ }
++ continue main_state_loop;
++ }
++
++ case STATE_AWAITING_TICK: {
++ final long deadline = startStateTask.getScheduledStart();
++ for (;;) {
++ if (this.state != startState) {
++ continue main_state_loop;
++ }
++ final long diff = deadline - System.nanoTime();
++ if (diff <= 0L) {
++ break;
++ }
++ LockSupport.parkNanos(startState, diff);
++ if (this.scheduler.halted) {
++ return;
++ }
++ }
++
++ if (!this.takeTask(startState, startStateTask)) {
++ continue main_state_loop;
++ }
++
++ // TODO exception handling
++ final boolean reschedule = startStateTask.runTick();
++
++ this.returnTask(startStateTask, reschedule);
++
++ continue main_state_loop;
++ }
++
++ case STATE_EXECUTING_TICK: {
++ throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread");
++ }
++
++ default: {
++ throw new IllegalStateException("Unknown state: " + startState);
++ }
++ }
++ }
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..82c4c11b0b564c97ac92bd5f54e3754a7ba95184
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
+@@ -0,0 +1,270 @@
++package ca.spottedleaf.concurrentutil.set;
++
++import java.util.Comparator;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++
++public final class LinkedSortedSet<E> implements Iterable<E> {
++
++ public final Comparator<? super E> comparator;
++
++ private Link<E> head;
++ private Link<E> tail;
++
++ public LinkedSortedSet() {
++ this((Comparator)Comparator.naturalOrder());
++ }
++
++ public LinkedSortedSet(final Comparator<? super E> comparator) {
++ this.comparator = comparator;
++ }
++
++ public void clear() {
++ this.head = this.tail = null;
++ }
++
++ public boolean isEmpty() {
++ return this.head == null;
++ }
++
++ public E first() {
++ final Link<E> head = this.head;
++ return head == null ? null : head.element;
++ }
++
++ public E last() {
++ final Link<E> tail = this.tail;
++ return tail == null ? null : tail.element;
++ }
++
++ public boolean containsFirst(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++ if (comparator.compare(element, curr.element) == 0) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public boolean containsLast(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++ if (comparator.compare(element, curr.element) == 0) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ private void removeNode(final Link<E> node) {
++ final Link<E> prev = node.prev;
++ final Link<E> next = node.next;
++
++ // help GC
++ node.element = null;
++ node.prev = null;
++ node.next = null;
++
++ if (prev == null) {
++ this.head = next;
++ } else {
++ prev.next = next;
++ }
++
++ if (next == null) {
++ this.tail = prev;
++ } else {
++ next.prev = prev;
++ }
++ }
++
++ public boolean remove(final Link<E> link) {
++ if (link.element == null) {
++ return false;
++ }
++
++ this.removeNode(link);
++ return true;
++ }
++
++ public boolean removeFirst(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++ if (comparator.compare(element, curr.element) == 0) {
++ this.removeNode(curr);
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public boolean removeLast(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++ if (comparator.compare(element, curr.element) == 0) {
++ this.removeNode(curr);
++ return true;
++ }
++ }
++ return false;
++ }
++
++ @Override
++ public Iterator<E> iterator() {
++ return new Iterator<>() {
++ private Link<E> next = LinkedSortedSet.this.head;
++
++ @Override
++ public boolean hasNext() {
++ return this.next != null;
++ }
++
++ @Override
++ public E next() {
++ final Link<E> next = this.next;
++ if (next == null) {
++ throw new NoSuchElementException();
++ }
++ this.next = next.next;
++ return next.element;
++ }
++ };
++ }
++
++ public E pollFirst() {
++ final Link<E> head = this.head;
++ if (head == null) {
++ return null;
++ }
++
++ final E ret = head.element;
++ final Link<E> next = head.next;
++
++ // unlink head
++ this.head = next;
++ if (next == null) {
++ this.tail = null;
++ } else {
++ next.prev = null;
++ }
++
++ // help GC
++ head.element = null;
++ head.next = null;
++
++ return ret;
++ }
++
++ public E pollLast() {
++ final Link<E> tail = this.tail;
++ if (tail == null) {
++ return null;
++ }
++
++ final E ret = tail.element;
++ final Link<E> prev = tail.prev;
++
++ // unlink tail
++ this.tail = prev;
++ if (prev == null) {
++ this.head = null;
++ } else {
++ prev.next = null;
++ }
++
++ // help GC
++ tail.element = null;
++ tail.prev = null;
++
++ return ret;
++ }
++
++ public Link<E> addLast(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++
++ Link<E> curr = this.tail;
++ if (curr != null) {
++ int compare;
++
++ while ((compare = comparator.compare(element, curr.element)) < 0) {
++ Link<E> prev = curr;
++ curr = curr.prev;
++ if (curr != null) {
++ continue;
++ }
++ return this.head = prev.prev = new Link<>(element, null, prev);
++ }
++
++ if (compare != 0) {
++ // insert after curr
++ final Link<E> next = curr.next;
++ final Link<E> insert = new Link<>(element, curr, next);
++ curr.next = insert;
++
++ if (next == null) {
++ this.tail = insert;
++ } else {
++ next.prev = insert;
++ }
++ return insert;
++ }
++
++ return null;
++ } else {
++ return this.head = this.tail = new Link<>(element);
++ }
++ }
++
++ public Link<E> addFirst(final E element) {
++ final Comparator<? super E> comparator = this.comparator;
++
++ Link<E> curr = this.head;
++ if (curr != null) {
++ int compare;
++
++ while ((compare = comparator.compare(element, curr.element)) > 0) {
++ Link<E> prev = curr;
++ curr = curr.next;
++ if (curr != null) {
++ continue;
++ }
++ return this.tail = prev.next = new Link<>(element, prev, null);
++ }
++
++ if (compare != 0) {
++ // insert before curr
++ final Link<E> prev = curr.prev;
++ final Link<E> insert = new Link<>(element, prev, curr);
++ curr.prev = insert;
++
++ if (prev == null) {
++ this.head = insert;
++ } else {
++ prev.next = insert;
++ }
++ return insert;
++ }
++
++ return null;
++ } else {
++ return this.head = this.tail = new Link<>(element);
++ }
++ }
++
++ public static final class Link<E> {
++ private E element;
++ private Link<E> prev;
++ private Link<E> next;
++
++ private Link(final E element) {
++ this.element = element;
++ }
++
++ private Link(final E element, final Link<E> prev, final Link<E> next) {
++ this.element = element;
++ this.prev = prev;
++ this.next = next;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bd8eb4f25d1dee00fbf9c05c14b0d94c5c641a55
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java
+@@ -0,0 +1,204 @@
++package ca.spottedleaf.concurrentutil.set;
++
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++import java.util.Objects;
++
++public final class LinkedUnsortedList<E> implements Iterable<E> {
++
++ private Link<E> head;
++ private Link<E> tail;
++
++ public LinkedUnsortedList() {}
++
++ public void clear() {
++ this.head = this.tail = null;
++ }
++
++ public boolean isEmpty() {
++ return this.head == null;
++ }
++
++ public E first() {
++ final Link<E> head = this.head;
++ return head == null ? null : head.element;
++ }
++
++ public E last() {
++ final Link<E> tail = this.tail;
++ return tail == null ? null : tail.element;
++ }
++
++ public boolean containsFirst(final E element) {
++ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++ if (Objects.equals(element, curr.element)) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public boolean containsLast(final E element) {
++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++ if (Objects.equals(element, curr.element)) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ private void removeNode(final Link<E> node) {
++ final Link<E> prev = node.prev;
++ final Link<E> next = node.next;
++
++ // help GC
++ node.element = null;
++ node.prev = null;
++ node.next = null;
++
++ if (prev == null) {
++ this.head = next;
++ } else {
++ prev.next = next;
++ }
++
++ if (next == null) {
++ this.tail = prev;
++ } else {
++ next.prev = prev;
++ }
++ }
++
++ public boolean remove(final Link<E> link) {
++ if (link.element == null) {
++ return false;
++ }
++
++ this.removeNode(link);
++ return true;
++ }
++
++ public boolean removeFirst(final E element) {
++ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++ if (Objects.equals(element, curr.element)) {
++ this.removeNode(curr);
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public boolean removeLast(final E element) {
++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++ if (Objects.equals(element, curr.element)) {
++ this.removeNode(curr);
++ return true;
++ }
++ }
++ return false;
++ }
++
++ @Override
++ public Iterator<E> iterator() {
++ return new Iterator<>() {
++ private Link<E> next = LinkedUnsortedList.this.head;
++
++ @Override
++ public boolean hasNext() {
++ return this.next != null;
++ }
++
++ @Override
++ public E next() {
++ final Link<E> next = this.next;
++ if (next == null) {
++ throw new NoSuchElementException();
++ }
++ this.next = next.next;
++ return next.element;
++ }
++ };
++ }
++
++ public E pollFirst() {
++ final Link<E> head = this.head;
++ if (head == null) {
++ return null;
++ }
++
++ final E ret = head.element;
++ final Link<E> next = head.next;
++
++ // unlink head
++ this.head = next;
++ if (next == null) {
++ this.tail = null;
++ } else {
++ next.prev = null;
++ }
++
++ // help GC
++ head.element = null;
++ head.next = null;
++
++ return ret;
++ }
++
++ public E pollLast() {
++ final Link<E> tail = this.tail;
++ if (tail == null) {
++ return null;
++ }
++
++ final E ret = tail.element;
++ final Link<E> prev = tail.prev;
++
++ // unlink tail
++ this.tail = prev;
++ if (prev == null) {
++ this.head = null;
++ } else {
++ prev.next = null;
++ }
++
++ // help GC
++ tail.element = null;
++ tail.prev = null;
++
++ return ret;
++ }
++
++ public Link<E> addLast(final E element) {
++ final Link<E> curr = this.tail;
++ if (curr != null) {
++ return this.tail = new Link<>(element, curr, null);
++ } else {
++ return this.head = this.tail = new Link<>(element);
++ }
++ }
++
++ public Link<E> addFirst(final E element) {
++ final Link<E> curr = this.head;
++ if (curr != null) {
++ return this.head = new Link<>(element, null, curr);
++ } else {
++ return this.head = this.tail = new Link<>(element);
++ }
++ }
++
++ public static final class Link<E> {
++ private E element;
++ private Link<E> prev;
++ private Link<E> next;
++
++ private Link(final E element) {
++ this.element = element;
++ }
++
++ private Link(final E element, final Link<E> prev, final Link<E> next) {
++ this.element = element;
++ this.prev = prev;
++ this.next = next;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9420b9822de99d3a31224642452835b0c986f7b4
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
+@@ -0,0 +1,31 @@
++package ca.spottedleaf.concurrentutil.util;
++
++import java.util.Collection;
++
++public final class CollectionUtil {
++
++ public static String toString(final Collection<?> collection, final String name) {
++ return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString();
++ }
++
++ public static StringBuilder toString(final Collection<?> collection, final String name, final StringBuilder builder) {
++ builder.append(name).append("{elements={");
++
++ boolean first = true;
++
++ for (final Object element : collection) {
++ if (!first) {
++ builder.append(", ");
++ }
++ first = false;
++
++ builder.append('"').append(element).append('"');
++ }
++
++ return builder.append("}}");
++ }
++
++ private CollectionUtil() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..23ae82e55696a7e2ff0e0f9609c0df6a48bb8d1d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
+@@ -0,0 +1,166 @@
++package ca.spottedleaf.concurrentutil.util;
++
++import java.lang.invoke.MethodHandles;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.locks.LockSupport;
++
++public final class ConcurrentUtil {
++
++ public static String genericToString(final Object object) {
++ return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString();
++ }
++
++ public static void rethrow(Throwable exception) {
++ rethrow0(exception);
++ }
++
++ private static <T extends Throwable> void rethrow0(Throwable thr) throws T {
++ throw (T)thr;
++ }
++
++ public static VarHandle getVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
++ try {
++ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType);
++ } catch (final Exception ex) {
++ throw new RuntimeException(ex); // unreachable
++ }
++ }
++
++ public static VarHandle getStaticVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
++ try {
++ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType);
++ } catch (final Exception ex) {
++ throw new RuntimeException(ex); // unreachable
++ }
++ }
++
++ /**
++ * Non-exponential backoff algorithm to use in lightly contended areas.
++ * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
++ * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
++ */
++ public static void backoff() {
++ Thread.onSpinWait();
++ }
++
++ /**
++ * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be
++ * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}.
++ * @param counter The current counter.
++ * @return The counter plus 1.
++ * @see ConcurrentUtil#backoff()
++ * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
++ */
++ public static long exponentiallyBackoffSimple(final long counter) {
++ for (long i = 0; i < counter; ++i) {
++ backoff();
++ }
++ return counter + 1L;
++ }
++
++ /**
++ * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread
++ * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange.
++ * @param counter The current counter.
++ * @return The next (if any) step in the backoff logic.
++ * @see ConcurrentUtil#backoff()
++ * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
++ */
++ public static long exponentiallyBackoffComplex(final long counter) {
++ // TODO experimentally determine counters
++ if (counter < 100L) {
++ return exponentiallyBackoffSimple(counter);
++ }
++ if (counter < 1_200L) {
++ Thread.yield();
++ LockSupport.parkNanos(1_000L);
++ return counter + 1L;
++ }
++ // scale 0.1ms (100us) per failure
++ Thread.yield();
++ LockSupport.parkNanos(100_000L * counter);
++ return counter + 1;
++ }
++
++ /**
++ * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
++ * @param counter The current failure counter.
++ * @param scale Time per failure, in ns.
++ * @param max The maximum time to wait for, in ns.
++ * @return The next counter.
++ */
++ public static long linearLongBackoff(long counter, final long scale, long max) {
++ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
++ max = Math.max(0, max);
++
++ if (scale <= 0L) {
++ return counter;
++ }
++
++ long time = scale * counter;
++
++ if (time > max || time / scale != counter) {
++ time = max;
++ }
++
++ boolean interrupted = Thread.interrupted();
++ if (time > 1_000_000L) { // 1ms
++ Thread.yield();
++ }
++ LockSupport.parkNanos(time);
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++ return counter;
++ }
++
++ /**
++ * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
++ * @param counter The current failure counter.
++ * @param scale Time per failure, in ns.
++ * @param max The maximum time to wait for, in ns.
++ * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}.
++ * @return The next counter.
++ */
++ public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) {
++ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
++ max = Math.max(0, max);
++
++ if (scale <= 0L) {
++ return counter;
++ }
++
++ long time = scale * counter;
++
++ // check overflow
++ if (time / scale != counter) {
++ // overflew
++ --counter;
++ time = max;
++ } else if (time > max) {
++ time = max;
++ }
++
++ final long currTime = System.nanoTime();
++ final long diff = deadline - currTime;
++ if (diff <= 0) {
++ return counter;
++ }
++ if (diff <= 1_500_000L) { // 1.5ms
++ time = 100_000L; // 100us
++ } else if (time > 1_000_000L) { // 1ms
++ Thread.yield();
++ }
++
++ boolean interrupted = Thread.interrupted();
++ LockSupport.parkNanos(time);
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++ return counter;
++ }
++
++ public static VarHandle getArrayHandle(final Class<?> type) {
++ return MethodHandles.arrayElementVarHandle(type);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2b9f36211d1cbb4fcf1457c0a83592499e9aa23b
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
+@@ -0,0 +1,111 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class HashUtil {
++
++ // Copied from fastutil HashCommon
++
++ /** 2<sup>32</sup> &middot; &phi;, &phi; = (&#x221A;5 &minus; 1)/2. */
++ private static final int INT_PHI = 0x9E3779B9;
++ /** The reciprocal of {@link #INT_PHI} modulo 2<sup>32</sup>. */
++ private static final int INV_INT_PHI = 0x144cbc89;
++ /** 2<sup>64</sup> &middot; &phi;, &phi; = (&#x221A;5 &minus; 1)/2. */
++ private static final long LONG_PHI = 0x9E3779B97F4A7C15L;
++ /** The reciprocal of {@link #LONG_PHI} modulo 2<sup>64</sup>. */
++ private static final long INV_LONG_PHI = 0xf1de83e19937733dL;
++
++ /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3.
++ *
++ * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
++ * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
++ *
++ * @param x an integer.
++ * @return a hash value with good avalanching properties.
++ */
++ // additional note: this function is a bijection onto all integers
++ public static int murmurHash3(int x) {
++ x ^= x >>> 16;
++ x *= 0x85ebca6b;
++ x ^= x >>> 13;
++ x *= 0xc2b2ae35;
++ x ^= x >>> 16;
++ return x;
++ }
++
++
++ /** Avalanches the bits of a long integer by applying the finalisation step of MurmurHash3.
++ *
++ * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
++ * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
++ *
++ * @param x a long integer.
++ * @return a hash value with good avalanching properties.
++ */
++ // additional note: this function is a bijection onto all longs
++ public static long murmurHash3(long x) {
++ x ^= x >>> 33;
++ x *= 0xff51afd7ed558ccdL;
++ x ^= x >>> 33;
++ x *= 0xc4ceb9fe1a85ec53L;
++ x ^= x >>> 33;
++ return x;
++ }
++
++ /** Quickly mixes the bits of an integer.
++ *
++ * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
++ * xorshifting the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
++ * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average number of probes
++ * is slightly larger), but it's much faster.
++ *
++ * @param x an integer.
++ * @return a hash value obtained by mixing the bits of {@code x}.
++ * @see #invMix(int)
++ */
++ // additional note: this function is a bijection onto all integers
++ public static int mix(final int x) {
++ final int h = x * INT_PHI;
++ return h ^ (h >>> 16);
++ }
++
++ /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests.
++ *
++ * @param x an integer.
++ * @return a value that passed through {@link #mix(int)} would give {@code x}.
++ */
++ // additional note: this function is a bijection onto all integers
++ public static int invMix(final int x) {
++ return (x ^ x >>> 16) * INV_INT_PHI;
++ }
++
++ /** Quickly mixes the bits of a long integer.
++ *
++ * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
++ * xorshifting twice the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
++ * it has slightly worse behaviour than {@link #murmurHash3(long)} (in open-addressing hash tables the average number of probes
++ * is slightly larger), but it's much faster.
++ *
++ * @param x a long integer.
++ * @return a hash value obtained by mixing the bits of {@code x}.
++ */
++ // additional note: this function is a bijection onto all longs
++ public static long mix(final long x) {
++ long h = x * LONG_PHI;
++ h ^= h >>> 32;
++ return h ^ (h >>> 16);
++ }
++
++ /** The inverse of {@link #mix(long)}. This method is mainly useful to create unit tests.
++ *
++ * @param x a long integer.
++ * @return a value that passed through {@link #mix(long)} would give {@code x}.
++ */
++ // additional note: this function is a bijection onto all longs
++ public static long invMix(long x) {
++ x ^= x >>> 32;
++ x ^= x >>> 16;
++ return (x ^ x >>> 32) * INV_LONG_PHI;
++ }
++
++
++ private HashUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4e61c477a56e645228d5a2015c26816954d17bf8
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
+@@ -0,0 +1,46 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class IntPairUtil {
++
++ /**
++ * Packs the specified integers into one long value.
++ */
++ public static long key(final int left, final int right) {
++ return ((long)right << 32) | (left & 0xFFFFFFFFL);
++ }
++
++ /**
++ * Retrieves the left packed integer from the key
++ */
++ public static int left(final long key) {
++ return (int)key;
++ }
++
++ /**
++ * Retrieves the right packed integer from the key
++ */
++ public static int right(final long key) {
++ return (int)(key >>> 32);
++ }
++
++ public static String toString(final long key) {
++ return "{left:" + left(key) + ", right:" + right(key) + "}";
++ }
++
++ public static String toString(final long[] array, final int from, final int to) {
++ final StringBuilder ret = new StringBuilder();
++ ret.append("[");
++
++ for (int i = from; i < to; ++i) {
++ if (i != from) {
++ ret.append(", ");
++ }
++ ret.append(toString(array[i]));
++ }
++
++ ret.append("]");
++ return ret.toString();
++ }
++
++ private IntPairUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9d7b9b8158cd01d12adbd7896ff77bee9828e101
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+@@ -0,0 +1,196 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class IntegerUtil {
++
++ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
++ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
++
++ public static int ceilLog2(final int value) {
++ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ }
++
++ public static long ceilLog2(final long value) {
++ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ }
++
++ public static int floorLog2(final int value) {
++ // xor is optimized subtract for 2^n -1
++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
++ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ }
++
++ public static int floorLog2(final long value) {
++ // xor is optimized subtract for 2^n -1
++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
++ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ }
++
++ public static int roundCeilLog2(final int value) {
++ // optimized variant of 1 << (32 - leading(val - 1))
++ // given
++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
++ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
++ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
++ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
++ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
++ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
++ }
++
++ public static long roundCeilLog2(final long value) {
++ // see logic documented above
++ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
++ }
++
++ public static int roundFloorLog2(final int value) {
++ // optimized variant of 1 << (31 - leading(val))
++ // given
++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
++ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
++ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
++ // HIGH_BIT_32 >> (31 - 31 + leading(val))
++ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
++ }
++
++ public static long roundFloorLog2(final long value) {
++ // see logic documented above
++ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
++ }
++
++ public static boolean isPowerOfTwo(final int n) {
++ // 2^n has one bit
++ // note: this rets true for 0 still
++ return IntegerUtil.getTrailingBit(n) == n;
++ }
++
++ public static boolean isPowerOfTwo(final long n) {
++ // 2^n has one bit
++ // note: this rets true for 0 still
++ return IntegerUtil.getTrailingBit(n) == n;
++ }
++
++ public static int getTrailingBit(final int n) {
++ return -n & n;
++ }
++
++ public static long getTrailingBit(final long n) {
++ return -n & n;
++ }
++
++ public static int trailingZeros(final int n) {
++ return Integer.numberOfTrailingZeros(n);
++ }
++
++ public static int trailingZeros(final long n) {
++ return Long.numberOfTrailingZeros(n);
++ }
++
++ // from hacker's delight (signed division magic value)
++ public static int getDivisorMultiple(final long numbers) {
++ return (int)(numbers >>> 32);
++ }
++
++ // from hacker's delight (signed division magic value)
++ public static int getDivisorShift(final long numbers) {
++ return (int)numbers;
++ }
++
++ // copied from hacker's delight (signed division magic value)
++ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
++ public static long getDivisorNumbers(final int d) {
++ final int ad = branchlessAbs(d);
++
++ if (ad < 2) {
++ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
++ }
++
++ final int two31 = 0x80000000;
++ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
++
++ /*
++ Signed usage:
++ int number;
++ long magic = getDivisorNumbers(div);
++ long mul = magic >>> 32;
++ int sign = number >> 31;
++ int result = (int)(((long)number * mul) >>> magic) - sign;
++ */
++ /*
++ Unsigned usage: (note: fails for input > Integer.MAX_VALUE, only use when input < Integer.MAX_VALUE to avoid sign calculation)
++ int number;
++ long magic = getDivisorNumbers(div);
++ long mul = magic >>> 32;
++ int result = (int)(((long)number * mul) >>> magic);
++ */
++
++ int p = 31;
++
++ // all these variables are UNSIGNED!
++ int t = two31 + (d >>> 31);
++ int anc = t - 1 - (int)((t & mask)%ad);
++ int q1 = (int)((two31 & mask)/(anc & mask));
++ int r1 = two31 - q1*anc;
++ int q2 = (int)((two31 & mask)/(ad & mask));
++ int r2 = two31 - q2*ad;
++ int delta;
++
++ do {
++ p = p + 1;
++ q1 = 2*q1; // Update q1 = 2**p/|nc|.
++ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
++ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
++ q1 = q1 + 1;
++ r1 = r1 - anc;
++ }
++ q2 = 2*q2; // Update q2 = 2**p/|d|.
++ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
++ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
++ q2 = q2 + 1;
++ r2 = r2 - ad;
++ }
++ delta = ad - r2;
++ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
++
++ int magicNum = q2 + 1;
++ if (d < 0) {
++ magicNum = -magicNum;
++ }
++ int shift = p;
++ return ((long)magicNum << 32) | shift;
++ }
++
++ public static int branchlessAbs(final int val) {
++ // -n = -1 ^ n + 1
++ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ }
++
++ public static long branchlessAbs(final long val) {
++ // -n = -1 ^ n + 1
++ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ }
++
++ // https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide
++ /**
++ *
++ * Usage:
++ * <pre>
++ * {@code
++ * static final long mult = getSimpleMultiplier(divisor, bits);
++ * long x = ...;
++ * long magic = x * mult;
++ * long divQ = magic >>> bits;
++ * long divR = ((magic & ((1 << bits) - 1)) * divisor) >>> bits;
++ * }
++ * </pre>
++ *
++ * @param bits The number of bits of precision for the returned result
++ */
++ public static long getUnsignedDivisorMagic(final long divisor, final int bits) {
++ return (((1L << bits) - 1L) / divisor) + 1;
++ }
++
++ private IntegerUtil() {
++ throw new RuntimeException();
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2919bbaa07b70f182438c3be8f9ebbe0649809b6
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java
+@@ -0,0 +1,145 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public enum Priority {
++
++ /**
++ * Priority value indicating the task has completed or is being completed.
++ * This priority cannot be used to schedule tasks.
++ */
++ COMPLETING(-1),
++
++ /**
++ * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
++ */
++ BLOCKING(),
++
++ /**
++ * Should only be used for urgent but not time-critical tasks.
++ */
++ HIGHEST(),
++
++ /**
++ * Two priorities above normal.
++ */
++ HIGHER(),
++
++ /**
++ * One priority above normal.
++ */
++ HIGH(),
++
++ /**
++ * Default priority.
++ */
++ NORMAL(),
++
++ /**
++ * One priority below normal.
++ */
++ LOW(),
++
++ /**
++ * Two priorities below normal.
++ */
++ LOWER(),
++
++ /**
++ * Use for tasks that should eventually execute, but are not needed to.
++ */
++ LOWEST(),
++
++ /**
++ * Use for tasks that can be delayed indefinitely.
++ */
++ IDLE();
++
++ // returns whether the priority can be scheduled
++ public static boolean isValidPriority(final Priority priority) {
++ return priority != null && priority != priority.COMPLETING;
++ }
++
++ // returns the higher priority of the two
++ public static Priority max(final Priority p1, final Priority p2) {
++ return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
++ }
++
++ // returns the lower priroity of the two
++ public static Priority min(final Priority p1, final Priority p2) {
++ return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
++ }
++
++ public boolean isHigherOrEqualPriority(final Priority than) {
++ return this.priority <= than.priority;
++ }
++
++ public boolean isHigherPriority(final Priority than) {
++ return this.priority < than.priority;
++ }
++
++ public boolean isLowerOrEqualPriority(final Priority than) {
++ return this.priority >= than.priority;
++ }
++
++ public boolean isLowerPriority(final Priority than) {
++ return this.priority > than.priority;
++ }
++
++ public boolean isHigherOrEqualPriority(final int than) {
++ return this.priority <= than;
++ }
++
++ public boolean isHigherPriority(final int than) {
++ return this.priority < than;
++ }
++
++ public boolean isLowerOrEqualPriority(final int than) {
++ return this.priority >= than;
++ }
++
++ public boolean isLowerPriority(final int than) {
++ return this.priority > than;
++ }
++
++ public static boolean isHigherOrEqualPriority(final int priority, final int than) {
++ return priority <= than;
++ }
++
++ public static boolean isHigherPriority(final int priority, final int than) {
++ return priority < than;
++ }
++
++ public static boolean isLowerOrEqualPriority(final int priority, final int than) {
++ return priority >= than;
++ }
++
++ public static boolean isLowerPriority(final int priority, final int than) {
++ return priority > than;
++ }
++
++ static final Priority[] PRIORITIES = Priority.values();
++
++ /** includes special priorities */
++ public static final int TOTAL_PRIORITIES = PRIORITIES.length;
++
++ public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
++
++ public static Priority getPriority(final int priority) {
++ return PRIORITIES[priority + 1];
++ }
++
++ private static int priorityCounter;
++
++ private static int nextCounter() {
++ return priorityCounter++;
++ }
++
++ public final int priority;
++
++ private Priority() {
++ this(nextCounter());
++ }
++
++ private Priority(final int priority) {
++ this.priority = priority;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a3a8b5c6795c4d116e094e4c910553416f565b93
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
+@@ -0,0 +1,11 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class ThrowUtil {
++
++ private ThrowUtil() {}
++
++ public static <T extends Throwable> void throwUnchecked(final Throwable thr) throws T {
++ throw (T)thr;
++ }
++
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
+@@ -0,0 +1,60 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class TimeUtil {
++
++ /*
++ * The comparator is not a valid comparator for every long value. To prove where it is valid, see below.
++ *
++ * For reflexivity, we have that x - x = 0. We then have that for any long value x that
++ * compareTimes(x, x) == 0, as expected.
++ *
++ * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE.
++ * So, the difference between any times x and y must not be equal to Long.MIN_VALUE.
++ *
++ * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that
++ * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values,
++ * this holds as long as a + b <= Long.MAX_VALUE.
++ *
++ * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have
++ * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE.
++ *
++ * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps
++ * c and d.
++ */
++
++ /**
++ * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which
++ * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2.
++ */
++ public static int compareTimes(final long t1, final long t2) {
++ final long diff = t1 - t2;
++
++ // HD, Section 2-7
++ return (int) ((diff >> 63) | (-diff >>> 63));
++ }
++
++ public static long getGreatestTime(final long t1, final long t2) {
++ final long diff = t1 - t2;
++ return diff < 0L ? t2 : t1;
++ }
++
++ public static long getLeastTime(final long t1, final long t2) {
++ final long diff = t1 - t2;
++ return diff > 0L ? t2 : t1;
++ }
++
++ public static long clampTime(final long value, final long min, final long max) {
++ final long diffMax = value - max;
++ final long diffMin = value - min;
++
++ if (diffMax > 0L) {
++ return max;
++ }
++ if (diffMin < 0L) {
++ return min;
++ }
++ return value;
++ }
++
++ private TimeUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
+@@ -0,0 +1,28 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class Validate {
++
++ public static <T> T notNull(final T obj) {
++ if (obj == null) {
++ throw new NullPointerException();
++ }
++ return obj;
++ }
++
++ public static <T> T notNull(final T obj, final String msgIfNull) {
++ if (obj == null) {
++ throw new NullPointerException(msgIfNull);
++ }
++ return obj;
++ }
++
++ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
++ if (off < 0 || len < 0 || (arrayLength - off) < len) {
++ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
++ }
++ }
++
++ private Validate() {
++ throw new RuntimeException();
++ }
++}
diff --git a/patches/server/0008-CB-fixes.patch b/patches/server/0008-CB-fixes.patch
new file mode 100644
index 0000000000..d4c409c72b
--- /dev/null
+++ b/patches/server/0008-CB-fixes.patch
@@ -0,0 +1,157 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Fri, 25 Feb 2022 07:14:48 -0800
+Subject: [PATCH] CB fixes
+
+* Missing Level -> LevelStem generic in StructureCheck
+ Need to use the right for injectDatafixingContext (Spottedleaf)
+
+* Fix summon_entity effect attempting to add incorrect entity (granny)
+
+* Removed incorrect parent perm for `minecraft.debugstick.always` (Machine_Maker)
+
+* Fixed method signature of Marker#addPassenger (Machine_Maker)
+
+* Removed unneeded UOE in CustomWorldChunkManager (extends BiomeSource) (Machine_Maker)
+
+* Honor Server#getLootTable method contract (Machine_Maker)
+
+Co-authored-by: Spottedleaf <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 67d5aaa5faa14e5ea5213efc6b24ef5b97fc17f7..ecbef5d54aef8e3f3bc2e4c34d2da6e96b1267b8 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -294,7 +294,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ long l = minecraftserver.getWorldData().worldGenOptions().seed();
+
+- this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), minecraftserver.getStructureManager(), resourcekey, chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer);
++ this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), minecraftserver.getStructureManager(), this.getTypeKey(), chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer); // Paper - Fix missing CB diff
+ this.structureManager = new StructureManager(this, this.serverLevelData.worldGenOptions(), this.structureCheck); // CraftBukkit
+ if ((this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END
+ this.dragonFight = new EndDragonFight(this, this.serverLevelData.worldGenOptions().seed(), this.serverLevelData.endDragonFightData()); // CraftBukkit
+diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+index 4f68394a94308513269f0a4c749b6a36738e3ca0..953ab7638f7242b5a11dd1de8786172443a0558c 100644
+--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+@@ -40,7 +40,7 @@ public class StructureCheck {
+ private final ChunkScanAccess storageAccess;
+ private final RegistryAccess registryAccess;
+ private final StructureTemplateManager structureTemplateManager;
+- private final ResourceKey<Level> dimension;
++ private final ResourceKey<net.minecraft.world.level.dimension.LevelStem> dimension; // Paper - fix missing CB diff
+ private final ChunkGenerator chunkGenerator;
+ private final RandomState randomState;
+ private final LevelHeightAccessor heightAccessor;
+@@ -54,7 +54,7 @@ public class StructureCheck {
+ ChunkScanAccess chunkIoWorker,
+ RegistryAccess registryManager,
+ StructureTemplateManager structureTemplateManager,
+- ResourceKey<Level> worldKey,
++ ResourceKey<net.minecraft.world.level.dimension.LevelStem> worldKey, // Paper - fix missing CB diff
+ ChunkGenerator chunkGenerator,
+ RandomState noiseConfig,
+ LevelHeightAccessor world,
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+index 19f165b855a0ca10732fd43c7ee093b11e535471..d54e0ab739ad33b8222d9ea2766e2a893154ee26 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+@@ -424,7 +424,7 @@ public class CraftChunk implements Chunk {
+ }
+ }
+
+- return new CraftChunkSnapshot(x, z, world.getMinHeight(), world.getMaxHeight(), world.getSeaLevel(), world.getName(), world.getFullTime(), blockIDs, skyLight, emitLight, empty, new Heightmap(actual, Heightmap.Types.MOTION_BLOCKING), iregistry, biome);
++ return new CraftChunkSnapshot(x, z, world.getMinHeight(), world.getMaxY(), world.getSeaLevel(), world.getName(), world.getFullTime(), blockIDs, skyLight, emitLight, empty, new Heightmap(actual, Heightmap.Types.MOTION_BLOCKING), iregistry, biome);
+ }
+
+ static void validateChunkCoordinates(int minY, int maxY, int x, int y, int z) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
+index a70e6872add1c952a89e74be0e6d09a53cc16559..90b82ad996b2b85628c9a5ddeef9410150b7f70c 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
+@@ -187,4 +187,11 @@ public class CraftLootTable implements org.bukkit.loot.LootTable {
+ org.bukkit.loot.LootTable table = (org.bukkit.loot.LootTable) obj;
+ return table.getKey().equals(this.getKey());
+ }
++
++ // Paper start - satisfy equals/hashCode contract
++ @Override
++ public int hashCode() {
++ return java.util.Objects.hash(key);
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index abe22b2d9ab742fc58ec6dfe88f9fa337ba7d838..a9dec31bdafd6bae677e58143fe618d812b338b7 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -1300,6 +1300,10 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ return this.world.getMaxY() + 1;
+ }
+
++ public int getMaxY() {
++ return this.world.getMaxY();
++ }
++
+ @Override
+ public int getLogicalHeight() {
+ return this.world.dimensionType().logicalHeight();
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index 153041dc3b4df33bd63a8a4765b4aa80c911e50e..d2aa1d32a62e074b53f304a755d42687ba0422ee 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -123,6 +123,7 @@ public class Main {
+ this.acceptsAll(Main.asList("forceUpgrade"), "Whether to force a world upgrade");
+ this.acceptsAll(Main.asList("eraseCache"), "Whether to force cache erase during world upgrade");
+ this.acceptsAll(Main.asList("recreateRegionFiles"), "Whether to recreate region files during world upgrade");
++ this.accepts("safeMode", "Loads level with vanilla datapack only"); // Paper
+ this.acceptsAll(Main.asList("nogui"), "Disables the graphical console");
+
+ this.acceptsAll(Main.asList("nojline"), "Disables jline and emulates the vanilla console");
+diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+index a25ccafc861249a2309bd42f08a32601644de46f..a7b53187a24d11b8c91e8c50eeb907aca60891cb 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+@@ -26,6 +26,7 @@ import org.bukkit.scheduler.BukkitWorker;
+
+ /**
+ * The fundamental concepts for this implementation:
++ * <ul>
+ * <li>Main thread owns {@link #head} and {@link #currentTick}, but it may be read from any thread</li>
+ * <li>Main thread exclusively controls {@link #temp} and {@link #pending}.
+ * They are never to be accessed outside of the main thread; alternatives exist to prevent locking.</li>
+@@ -41,6 +42,7 @@ import org.bukkit.scheduler.BukkitWorker;
+ * <li>Sync tasks are only to be removed from runners on the main thread when coupled with a removal from pending and temp.</li>
+ * <li>Most of the design in this scheduler relies on queuing special tasks to perform any data changes on the main thread.
+ * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending()} calls.</li>
++ * </ul>
+ */
+ public class CraftScheduler implements BukkitScheduler {
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 592906e5b4cd7e3859001b279c1d5d70d2882c84..de7f9d5b3860e7d187d73a1bd0d28c70293ef66c 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -247,7 +247,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ try {
+ nmsStack.applyComponents(new ItemParser(Commands.createValidationContext(MinecraftServer.getDefaultRegistryAccess())).parse(new StringReader(arguments)).components());
+ } catch (CommandSyntaxException ex) {
+- Logger.getLogger(CraftMagicNumbers.class.getName()).log(Level.SEVERE, null, ex);
++ com.mojang.logging.LogUtils.getClassLogger().error("Exception modifying ItemStack", new Throwable(ex)); // Paper - show stack trace
+ }
+
+ stack.setItemMeta(CraftItemStack.getItemMeta(nmsStack));
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java
+index 5ac25dab93fd4c9e9533c80d1ca3d93446d7a365..245ad120a36b6defca7e6889faae1ca5fc33d0c7 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CraftDefaultPermissions.java
+@@ -15,7 +15,7 @@ public final class CraftDefaultPermissions {
+ DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".nbt.place", "Gives the user the ability to place restricted blocks with NBT in creative", org.bukkit.permissions.PermissionDefault.OP, parent);
+ DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".nbt.copy", "Gives the user the ability to copy NBT in creative", org.bukkit.permissions.PermissionDefault.TRUE, parent);
+ DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick", "Gives the user the ability to use the debug stick in creative", org.bukkit.permissions.PermissionDefault.OP, parent);
+- DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick.always", "Gives the user the ability to use the debug stick in all game modes", org.bukkit.permissions.PermissionDefault.FALSE, parent);
++ DefaultPermissions.registerPermission(CraftDefaultPermissions.ROOT + ".debugstick.always", "Gives the user the ability to use the debug stick in all game modes", org.bukkit.permissions.PermissionDefault.FALSE/* , parent */); // Paper - should not have this parent, as it's not a "vanilla" utility
+ // Spigot end
+ parent.recalculatePermissibles();
+ }
diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch
new file mode 100644
index 0000000000..e2c27693b4
--- /dev/null
+++ b/patches/server/0009-MC-Utils.patch
@@ -0,0 +1,6589 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 28 Mar 2016 20:55:47 -0400
+Subject: [PATCH] MC Utils
+
+== AT ==
+public net.minecraft.server.level.ServerChunkCache mainThread
+public net.minecraft.server.level.ServerLevel chunkSource
+public org.bukkit.craftbukkit.inventory.CraftItemStack handle
+public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder;
+public net.minecraft.server.level.ServerChunkCache mainThreadProcessor
+public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
+public net.minecraft.world.level.chunk.LevelChunkSection states
+
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6c98d420ea84c10ef4f15d4deb3f04e610ed8548
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+@@ -0,0 +1,117 @@
++package ca.spottedleaf.moonrise.common;
++
++import com.mojang.datafixers.DSL;
++import com.mojang.datafixers.DataFixer;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.ServiceLoader;
++import java.util.function.Predicate;
++
++public interface PlatformHooks {
++ public static PlatformHooks get() {
++ return Holder.INSTANCE;
++ }
++
++ public String getBrand();
++
++ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos);
++
++ public Predicate<BlockState> maybeHasLightEmission();
++
++ public boolean hasCurrentlyLoadingChunk();
++
++ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder);
++
++ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk);
++
++ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original);
++
++ public boolean allowAsyncTicketUpdates();
++
++ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel);
++
++ public void chunkUnloadFromWorld(final LevelChunk chunk);
++
++ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data);
++
++ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player);
++
++ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player);
++
++ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate,
++ final List<Entity> into);
++
++ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest,
++ final AABB boundingBox, final Predicate<? super T> predicate,
++ final List<? super T> into, final int maxCount);
++
++ public void entityMove(final Entity entity, final long oldSection, final long newSection);
++
++ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event);
++
++ public boolean configFixMC224294();
++
++ public boolean configAutoConfigSendDistance();
++
++ public double configPlayerMaxLoadRate();
++
++ public double configPlayerMaxGenRate();
++
++ public double configPlayerMaxSendRate();
++
++ public int configPlayerMaxConcurrentLoads();
++
++ public int configPlayerMaxConcurrentGens();
++
++ public long configAutoSaveInterval(final ServerLevel world);
++
++ public int configMaxAutoSavePerTick(final ServerLevel world);
++
++ public boolean configFixMC159283();
++
++ // support for CB chunk mustNotSave
++ public boolean forceNoSave(final ChunkAccess chunk);
++
++ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
++ final int fromVersion, final int toVersion);
++
++ public boolean hasMainChunkLoadHook();
++
++ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData);
++
++ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities);
++
++ public void unloadEntity(final Entity entity);
++
++ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk);
++
++ public int modifyEntityTrackingRange(final Entity entity, final int currentRange);
++
++ public static final class Holder {
++ private Holder() {
++ }
++
++ private static final PlatformHooks INSTANCE;
++
++ static {
++ INSTANCE = ServiceLoader.load(PlatformHooks.class, PlatformHooks.class.getClassLoader()).findFirst()
++ .orElseThrow(() -> new RuntimeException("Failed to locate PlatformHooks"));
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7fed43a1e7bcf35c4d7fd3224837a47fedd59860
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -0,0 +1,128 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
++import net.minecraft.world.entity.Entity;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++
++// list with O(1) remove & contains
++
++/**
++ * @author Spottedleaf
++ */
++public final class EntityList implements Iterable<Entity> {
++
++ private final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
++ {
++ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ private static final Entity[] EMPTY_LIST = new Entity[0];
++
++ private Entity[] entities = EMPTY_LIST;
++ private int count;
++
++ public int size() {
++ return this.count;
++ }
++
++ public boolean contains(final Entity entity) {
++ return this.entityToIndex.containsKey(entity.getId());
++ }
++
++ public boolean remove(final Entity entity) {
++ final int index = this.entityToIndex.remove(entity.getId());
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // move the entity at the end to this index
++ final int endIndex = --this.count;
++ final Entity end = this.entities[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.entityToIndex.put(end.getId(), index); // update index
++ }
++ this.entities[index] = end;
++ this.entities[endIndex] = null;
++
++ return true;
++ }
++
++ public boolean add(final Entity entity) {
++ final int count = this.count;
++ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ Entity[] list = this.entities;
++
++ if (list.length == count) {
++ // resize required
++ list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = entity;
++ this.count = count + 1;
++
++ return true;
++ }
++
++ public Entity getChecked(final int index) {
++ if (index < 0 || index >= this.count) {
++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
++ }
++ return this.entities[index];
++ }
++
++ public Entity getUnchecked(final int index) {
++ return this.entities[index];
++ }
++
++ public Entity[] getRawData() {
++ return this.entities;
++ }
++
++ public void clear() {
++ this.entityToIndex.clear();
++ Arrays.fill(this.entities, 0, this.count, null);
++ this.count = 0;
++ }
++
++ @Override
++ public Iterator<Entity> iterator() {
++ return new Iterator<>() {
++ private Entity lastRet;
++ private int current;
++
++ @Override
++ public boolean hasNext() {
++ return this.current < EntityList.this.count;
++ }
++
++ @Override
++ public Entity next() {
++ if (this.current >= EntityList.this.count) {
++ throw new NoSuchElementException();
++ }
++ return this.lastRet = EntityList.this.entities[this.current++];
++ }
++
++ @Override
++ public void remove() {
++ final Entity lastRet = this.lastRet;
++
++ if (lastRet == null) {
++ throw new IllegalStateException();
++ }
++ this.lastRet = null;
++
++ EntityList.this.remove(lastRet);
++ --this.current;
++ }
++ };
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9f3b25bb2439f283f878db93973a02fcdcd14eed
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
++import java.util.Arrays;
++
++public final class IntList {
++
++ private final Int2IntOpenHashMap map = new Int2IntOpenHashMap();
++ {
++ this.map.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ private static final int[] EMPTY_LIST = new int[0];
++
++ private int[] byIndex = EMPTY_LIST;
++ private int count;
++
++ public int size() {
++ return this.count;
++ }
++
++ public void setMinCapacity(final int len) {
++ final int[] byIndex = this.byIndex;
++ if (byIndex.length < len) {
++ this.byIndex = Arrays.copyOf(byIndex, len);
++ }
++ }
++
++ public int getRaw(final int index) {
++ return this.byIndex[index];
++ }
++
++ public boolean add(final int value) {
++ final int count = this.count;
++ final int currIndex = this.map.putIfAbsent(value, count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ int[] list = this.byIndex;
++
++ if (list.length == count) {
++ // resize required
++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = value;
++ this.count = count + 1;
++
++ return true;
++ }
++
++ public boolean remove(final int value) {
++ final int index = this.map.remove(value);
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // move the entry at the end to this index
++ final int endIndex = --this.count;
++ final int end = this.byIndex[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.map.put(end, index);
++ }
++ this.byIndex[index] = end;
++ this.byIndex[endIndex] = 0;
++
++ return true;
++ }
++
++ public void clear() {
++ this.count = 0;
++ this.map.clear();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c21e00812f1aaa1279834a0562d360d6b89e146c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
+@@ -0,0 +1,312 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
++
++public final class IteratorSafeOrderedReferenceSet<E> {
++
++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
++
++ private final Reference2IntLinkedOpenHashMap<E> indexMap;
++ private int firstInvalidIndex = -1;
++
++ /* list impl */
++ private E[] listElements;
++ private int listSize;
++
++ private final double maxFragFactor;
++
++ private int iteratorCount;
++
++ public IteratorSafeOrderedReferenceSet() {
++ this(16, 0.75f, 16, 0.2);
++ }
++
++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++ final double maxFragFactor) {
++ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
++ this.indexMap.defaultReturnValue(-1);
++ this.maxFragFactor = maxFragFactor;
++ this.listElements = (E[])new Object[arrayCapacity];
++ }
++
++ /*
++ public void check() {
++ int iterated = 0;
++ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
++ if (this.listElements != null) {
++ for (int i = 0; i < this.listSize; ++i) {
++ Object obj = this.listElements[i];
++ if (obj != null) {
++ iterated++;
++ if (!check.add((E)obj)) {
++ throw new IllegalStateException("contains duplicate");
++ }
++ if (!this.contains((E)obj)) {
++ throw new IllegalStateException("desync");
++ }
++ }
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++ }
++
++ check.clear();
++ iterated = 0;
++ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final E element = iterator.next();
++ iterated++;
++ if (!check.add(element)) {
++ throw new IllegalStateException("contains duplicate (iterator is wrong)");
++ }
++ if (!this.contains(element)) {
++ throw new IllegalStateException("desync (iterator is wrong)");
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
++ }
++ }
++ */
++
++ private double getFragFactor() {
++ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
++ }
++
++ public int createRawIterator() {
++ ++this.iteratorCount;
++ if (this.indexMap.isEmpty()) {
++ return -1;
++ } else {
++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++ }
++ }
++
++ public int advanceRawIterator(final int index) {
++ final E[] elements = this.listElements;
++ int ret = index + 1;
++ for (int len = this.listSize; ret < len; ++ret) {
++ if (elements[ret] != null) {
++ return ret;
++ }
++ }
++
++ return -1;
++ }
++
++ public void finishRawIterator() {
++ if (--this.iteratorCount == 0) {
++ if (this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ }
++ }
++
++ public boolean remove(final E element) {
++ final int index = this.indexMap.removeInt(element);
++ if (index >= 0) {
++ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
++ this.firstInvalidIndex = index;
++ }
++ if (this.listElements[index] != element) {
++ throw new IllegalStateException();
++ }
++ this.listElements[index] = null;
++ if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ //this.check();
++ return true;
++ }
++ return false;
++ }
++
++ public boolean contains(final E element) {
++ return this.indexMap.containsKey(element);
++ }
++
++ public boolean add(final E element) {
++ final int listSize = this.listSize;
++
++ final int previous = this.indexMap.putIfAbsent(element, listSize);
++ if (previous != -1) {
++ return false;
++ }
++
++ if (listSize >= this.listElements.length) {
++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
++ }
++ this.listElements[listSize] = element;
++ this.listSize = listSize + 1;
++
++ //this.check();
++ return true;
++ }
++
++ private void defrag() {
++ if (this.firstInvalidIndex < 0) {
++ return; // nothing to do
++ }
++
++ if (this.indexMap.isEmpty()) {
++ Arrays.fill(this.listElements, 0, this.listSize, null);
++ this.listSize = 0;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ return;
++ }
++
++ final E[] backingArray = this.listElements;
++
++ int lastValidIndex;
++ java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
++
++ if (this.firstInvalidIndex == 0) {
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
++ lastValidIndex = 0;
++ } else {
++ lastValidIndex = this.firstInvalidIndex;
++ final E key = backingArray[lastValidIndex - 1];
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
++ @Override
++ public int getIntValue() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public int setValue(int i) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public E getKey() {
++ return key;
++ }
++ });
++ }
++
++ while (iterator.hasNext()) {
++ final Reference2IntMap.Entry<E> entry = iterator.next();
++
++ final int newIndex = lastValidIndex++;
++ backingArray[newIndex] = entry.getKey();
++ entry.setValue(newIndex);
++ }
++
++ // cleanup end
++ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
++ this.listSize = lastValidIndex;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ }
++
++ public E rawGet(final int index) {
++ return this.listElements[index];
++ }
++
++ public int size() {
++ // always returns the correct amount - listSize can be different
++ return this.indexMap.size();
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() {
++ return this.iterator(0);
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
++ ++this.iteratorCount;
++ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public java.util.Iterator<E> unsafeIterator() {
++ return this.unsafeIterator(0);
++ }
++ public java.util.Iterator<E> unsafeIterator(final int flags) {
++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public static interface Iterator<E> extends java.util.Iterator<E> {
++
++ public void finishedIterating();
++
++ }
++
++ private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
++
++ private final IteratorSafeOrderedReferenceSet<E> set;
++ private final boolean canFinish;
++ private final int maxIndex;
++ private int nextIndex;
++ private E pendingValue;
++ private boolean finished;
++ private E lastReturned;
++
++ private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
++ this.set = set;
++ this.canFinish = canFinish;
++ this.maxIndex = maxIndex;
++ }
++
++ @Override
++ public boolean hasNext() {
++ if (this.finished) {
++ return false;
++ }
++ if (this.pendingValue != null) {
++ return true;
++ }
++
++ final E[] elements = this.set.listElements;
++ int index, len;
++ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
++ final E element = elements[index];
++ if (element != null) {
++ this.pendingValue = element;
++ this.nextIndex = index + 1;
++ return true;
++ }
++ }
++
++ this.nextIndex = index;
++ return false;
++ }
++
++ @Override
++ public E next() {
++ if (!this.hasNext()) {
++ throw new NoSuchElementException();
++ }
++ final E ret = this.pendingValue;
++
++ this.pendingValue = null;
++ this.lastReturned = ret;
++
++ return ret;
++ }
++
++ @Override
++ public void remove() {
++ final E lastReturned = this.lastReturned;
++ if (lastReturned == null) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.set.remove(lastReturned);
++ }
++
++ @Override
++ public void finishedIterating() {
++ if (this.finished || !this.canFinish) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.finished = true;
++ this.set.finishRawIterator();
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2e876b918672e8ef3b5197b7e6b1597247fdeaa1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+@@ -0,0 +1,142 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++
++public final class ReferenceList<E> implements Iterable<E> {
++
++ private static final Object[] EMPTY_LIST = new Object[0];
++
++ private final Reference2IntOpenHashMap<E> referenceToIndex;
++ private E[] references;
++ private int count;
++
++ public ReferenceList() {
++ this((E[])EMPTY_LIST);
++ }
++
++ public ReferenceList(final E[] referenceArray) {
++ this.references = referenceArray;
++ this.referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
++ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ private ReferenceList(final E[] references, final int count, final Reference2IntOpenHashMap<E> referenceToIndex) {
++ this.references = references;
++ this.count = count;
++ this.referenceToIndex = referenceToIndex;
++ }
++
++ public ReferenceList<E> copy() {
++ return new ReferenceList<>(this.references.clone(), this.count, this.referenceToIndex.clone());
++ }
++
++ public int size() {
++ return this.count;
++ }
++
++ public boolean contains(final E obj) {
++ return this.referenceToIndex.containsKey(obj);
++ }
++
++ public boolean remove(final E obj) {
++ final int index = this.referenceToIndex.removeInt(obj);
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // move the object at the end to this index
++ final int endIndex = --this.count;
++ final E end = (E)this.references[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.referenceToIndex.put(end, index); // update index
++ }
++ this.references[index] = end;
++ this.references[endIndex] = null;
++
++ return true;
++ }
++
++ public boolean add(final E obj) {
++ final int count = this.count;
++ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ E[] list = this.references;
++
++ if (list.length == count) {
++ // resize required
++ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = obj;
++ this.count = count + 1;
++
++ return true;
++ }
++
++ public E getChecked(final int index) {
++ if (index < 0 || index >= this.count) {
++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
++ }
++ return this.references[index];
++ }
++
++ public E getUnchecked(final int index) {
++ return this.references[index];
++ }
++
++ public Object[] getRawData() {
++ return this.references;
++ }
++
++ public E[] getRawDataUnchecked() {
++ return this.references;
++ }
++
++ public void clear() {
++ this.referenceToIndex.clear();
++ Arrays.fill(this.references, 0, this.count, null);
++ this.count = 0;
++ }
++
++ @Override
++ public Iterator<E> iterator() {
++ return new Iterator<>() {
++ private E lastRet;
++ private int current;
++
++ @Override
++ public boolean hasNext() {
++ return this.current < ReferenceList.this.count;
++ }
++
++ @Override
++ public E next() {
++ if (this.current >= ReferenceList.this.count) {
++ throw new NoSuchElementException();
++ }
++ return this.lastRet = ReferenceList.this.references[this.current++];
++ }
++
++ @Override
++ public void remove() {
++ final E lastRet = this.lastRet;
++
++ if (lastRet == null) {
++ throw new IllegalStateException();
++ }
++ this.lastRet = null;
++
++ ReferenceList.this.remove(lastRet);
++ --this.current;
++ }
++ };
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2bae9949ef325d0001aa638150fbbdf968367e75
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
++import java.util.Arrays;
++
++public final class ShortList {
++
++ private final Short2ShortOpenHashMap map = new Short2ShortOpenHashMap();
++ {
++ this.map.defaultReturnValue(Short.MIN_VALUE);
++ }
++
++ private static final short[] EMPTY_LIST = new short[0];
++
++ private short[] byIndex = EMPTY_LIST;
++ private short count;
++
++ public int size() {
++ return (int)this.count;
++ }
++
++ public short getRaw(final int index) {
++ return this.byIndex[index];
++ }
++
++ public void setMinCapacity(final int len) {
++ final short[] byIndex = this.byIndex;
++ if (byIndex.length < len) {
++ this.byIndex = Arrays.copyOf(byIndex, len);
++ }
++ }
++
++ public boolean add(final short value) {
++ final int count = (int)this.count;
++ final short currIndex = this.map.putIfAbsent(value, (short)count);
++
++ if (currIndex != Short.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ short[] list = this.byIndex;
++
++ if (list.length == count) {
++ // resize required
++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = value;
++ this.count = (short)(count + 1);
++
++ return true;
++ }
++
++ public boolean remove(final short value) {
++ final short index = this.map.remove(value);
++ if (index == Short.MIN_VALUE) {
++ return false;
++ }
++
++ // move the entry at the end to this index
++ final short endIndex = --this.count;
++ final short end = this.byIndex[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.map.put(end, index);
++ }
++ this.byIndex[(int)index] = end;
++ this.byIndex[(int)endIndex] = (short)0;
++
++ return true;
++ }
++
++ public void clear() {
++ this.count = (short)0;
++ this.map.clear();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..db92261a6cb3758391108361096417c61bc82cdc
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
+@@ -0,0 +1,117 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.Comparator;
++
++public final class SortedList<E> {
++
++ private static final Object[] EMPTY_LIST = new Object[0];
++
++ private Comparator<? super E> comparator;
++ private E[] elements;
++ private int count;
++
++ public SortedList(final Comparator<? super E> comparator) {
++ this((E[])EMPTY_LIST, comparator);
++ }
++
++ public SortedList(final E[] elements, final Comparator<? super E> comparator) {
++ this.elements = elements;
++ this.comparator = comparator;
++ }
++
++ // start, end are inclusive
++ private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> comparator,
++ int start, int end) {
++ while (start <= end) {
++ final int middle = (start + end) >>> 1;
++
++ final E middleVal = elements[middle];
++
++ final int cmp = comparator.compare(element, middleVal);
++
++ if (cmp < 0) {
++ end = middle - 1;
++ } else {
++ start = middle + 1;
++ }
++ }
++
++ return start;
++ }
++
++ public int size() {
++ return this.count;
++ }
++
++ public boolean isEmpty() {
++ return this.count == 0;
++ }
++
++ public int add(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ this.count = count + 1;
++ final Comparator<? super E> comparator = this.comparator;
++
++ final int idx = insertIdx(elements, element, comparator, 0, count - 1);
++
++ if (count >= elements.length) {
++ // copy and insert at the same time
++ if (idx == count) {
++ this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ elements[count] = element;
++ return idx;
++ } else {
++ final E[] newElements = (E[])Array.newInstance(elements.getClass().getComponentType(), (int)Math.max(4L, count * 2L));
++ System.arraycopy(elements, 0, newElements, 0, idx);
++ newElements[idx] = element;
++ System.arraycopy(elements, idx, newElements, idx + 1, count - idx);
++ this.elements = newElements;
++ return idx;
++ }
++ } else {
++ if (idx == count) {
++ // no copy needed
++ elements[idx] = element;
++ return idx;
++ } else {
++ // shift elements down
++ System.arraycopy(elements, idx, elements, idx + 1, count - idx);
++ elements[idx] = element;
++ return idx;
++ }
++ }
++ }
++
++ public E get(final int idx) {
++ if (idx < 0 || idx >= this.count) {
++ throw new IndexOutOfBoundsException(idx);
++ }
++ return this.elements[idx];
++ }
++
++
++ public E remove(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ final Comparator<? super E> comparator = this.comparator;
++
++ final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
++ if (idx < 0) {
++ return null;
++ }
++
++ final int last = this.count - 1;
++ this.count = last;
++
++ final E ret = elements[idx];
++
++ System.arraycopy(elements, idx + 1, elements, idx, last - idx);
++
++ elements[last] = null;
++
++ return ret;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..62caf61a4b0b7ebc764006ea8bbd0274594d9f4a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.ints.Int2IntFunction;
++
++import java.util.Arrays;
++
++public class Int2IntArraySortedMap {
++
++ protected int[] key;
++ protected int[] val;
++ protected int size;
++
++ public Int2IntArraySortedMap() {
++ this.key = new int[8];
++ this.val = new int[8];
++ }
++
++ public int put(final int key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return 0;
++ }
++
++ public int computeIfAbsent(final int key, final Int2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ public int get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
++ }
++ return this.val[index];
++ }
++
++ public int getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
++ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fea9e8ba7caaf6259614090d4f872619470d32f9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+@@ -0,0 +1,74 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import java.util.Arrays;
++import java.util.function.IntFunction;
++
++public class Int2ObjectArraySortedMap<V> {
++
++ protected int[] key;
++ protected V[] val;
++ protected int size;
++
++ public Int2ObjectArraySortedMap() {
++ this.key = new int[8];
++ this.val = (V[])new Object[8];
++ }
++
++ public V put(final int key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
++ }
++
++ public V computeIfAbsent(final int key, final IntFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ public V get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
++ }
++ return this.val[index];
++ }
++
++ public V getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1);
++ return this.val[insert];
++ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c077ca606934e9f13da3a8e2a194f82a99fe9ae9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2IntFunction;
++
++import java.util.Arrays;
++
++public class Long2IntArraySortedMap {
++
++ protected long[] key;
++ protected int[] val;
++ protected int size;
++
++ public Long2IntArraySortedMap() {
++ this.key = new long[8];
++ this.val = new int[8];
++ }
++
++ public int put(final long key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return 0;
++ }
++
++ public int computeIfAbsent(final long key, final Long2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ public int get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
++ }
++ return this.val[index];
++ }
++
++ public int getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
++ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b24d037af5709196b66c79c692e1814cd5b20e49
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+@@ -0,0 +1,76 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import java.util.Arrays;
++import java.util.function.LongFunction;
++
++public class Long2ObjectArraySortedMap<V> {
++
++ protected long[] key;
++ protected V[] val;
++ protected int size;
++
++ public Long2ObjectArraySortedMap() {
++ this.key = new long[8];
++ this.val = (V[])new Object[8];
++ }
++
++ public V put(final long key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
++ }
++
++ public V computeIfAbsent(final long key, final LongFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ public V get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
++ }
++ return this.val[index];
++ }
++
++ public V getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? null : this.val[insert];
++ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aa86882bb7b0712f29d7344009093c0e7a81be84
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+@@ -0,0 +1,48 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
++import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
++
++public final class SynchronisedLong2BooleanMap {
++ private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
++ private final int limit;
++
++ public SynchronisedLong2BooleanMap(final int limit) {
++ this.limit = limit;
++ }
++
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLastBoolean();
++ }
++ }
++
++ public boolean remove(final long key) {
++ synchronized (this.map) {
++ return this.map.remove(key);
++ }
++ }
++
++ // note:
++ public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ }
++
++ final boolean put = ifAbsent.get(key);
++
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ this.map.putAndMoveToFirst(key, put);
++
++ this.purgeEntries();
++
++ return put;
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..dbb51afc6cefe0071fe3ddcd2c1109f2755c3b4d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+@@ -0,0 +1,47 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import java.util.function.BiFunction;
++
++public final class SynchronisedLong2ObjectMap<V> {
++ private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
++ private final int limit;
++
++ public SynchronisedLong2ObjectMap(final int limit) {
++ this.limit = limit;
++ }
++
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLast();
++ }
++ }
++
++ public V get(final long key) {
++ synchronized (this.map) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ }
++
++ public V put(final long key, final V value) {
++ synchronized (this.map) {
++ final V ret = this.map.putAndMoveToFirst(key, value);
++ this.purgeEntries();
++ return ret;
++ }
++ }
++
++ public V compute(final long key, final BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
++ synchronized (this.map) {
++ // first, compute the value - if one is added, it will be at the last entry
++ this.map.compute(key, remappingFunction);
++ // move the entry to first, just in case it was added at last
++ final V ret = this.map.getAndMoveToFirst(key);
++ // now purge the last entries
++ this.purgeEntries();
++
++ return ret;
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9c0eff9017b24bb65b1029cefb5d0bfcb9beff01
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+@@ -0,0 +1,75 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++public final class AllocatingRateLimiter {
++
++ // max difference granularity in ns
++ private final long maxGranularity;
++
++ private double allocation = 0.0;
++ private long lastAllocationUpdate;
++ // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error)
++ // over any time period using take regardless of the number of take calls or the intervals between the take calls
++ // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3
++ private double takeCarry = 0.0;
++ private long lastTakeUpdate;
++
++ public AllocatingRateLimiter(final long maxGranularity) {
++ this.maxGranularity = maxGranularity;
++ }
++
++ public void reset(final long time) {
++ this.allocation = 0.0;
++ this.lastAllocationUpdate = time;
++ this.takeCarry = 0.0;
++ this.lastTakeUpdate = time;
++ }
++
++ // rate in units/s, and time in ns
++ public void tickAllocation(final long time, final double rate, final double maxAllocation) {
++ final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate);
++ this.lastAllocationUpdate = time;
++
++ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
++ }
++
++ public long previewAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
++
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ return (long)Math.floor(this.takeCarry + take);
++ }
++
++ // rate in units/s, and time in ns
++ public long takeAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
++
++ double ret = this.takeCarry;
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++ this.lastTakeUpdate = time;
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ ret += take;
++ this.allocation -= take;
++
++ final long retInteger = (long)Math.floor(ret);
++ this.takeCarry = ret - (double)retInteger;
++
++ return retInteger;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..460e27ab0506c83a28934800ee74ee886d4b025e
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+@@ -0,0 +1,297 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++
++public final class Delayed26WayDistancePropagator3D {
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed26WayDistancePropagator3D() {
++ this(null);
++ }
++
++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int y, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void setSource(final int x, final int y, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
++ }
++
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int y, final int z) {
++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // queues used for BFS propagating levels
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
++ }
++
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ab2fa1563d5e32a5313dfcc1da411cab45fb5ca0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+@@ -0,0 +1,718 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++
++public final class Delayed8WayDistancePropagator2D {
++
++ // Test
++ /*
++ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) {
++ int got = test.getLevel(x, z);
++
++ int expect = 0;
++ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet();
++ if (nearest != null) {
++ for (Object _obj : nearest) {
++ if (_obj instanceof Ticket) {
++ Ticket ticket = (Ticket)_obj;
++ long ticketCoord = reference.getLastCoordinate(ticket);
++ int viewDistance = reference.getLastViewDistance(ticket);
++ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x),
++ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z));
++ int level = viewDistance - distance;
++ if (level > expect) {
++ expect = level;
++ }
++ }
++ }
++ }
++
++ if (expect != got) {
++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
++ }
++ }
++
++ static class Ticket {
++
++ int x;
++ int z;
++
++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty
++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
++
++ }
++
++ public static void main(final String[] args) {
++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
++ @Override
++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) {
++ return object.empty;
++ }
++ };
++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
++
++ final int maxDistance = 64;
++ // test origin
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ // test single source
++ reference.add(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, 0, 0, originDistance/2);
++ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(0, 0); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = (i & 1) == 1 ? -i : i;
++ a.z = (i & 1) == 1 ? -i : i;
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++
++ // now test at coordinate offsets
++ // test offset
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ // test single source
++ reference.add(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, offX, offZ, originDistance/2);
++ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(offX, offZ); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = offX + ((i & 1) == 1 ? -i : i);
++ a.z = offZ + ((i & 1) == 1 ? -i : i);
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++ }
++ */
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed8WayDistancePropagator2D() {
++ this(null);
++ }
++
++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkKey(x, z));
++ }
++
++ public void setSource(final int x, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkKey(x, z), level);
++ }
++
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int z) {
++ this.removeSource(CoordinateUtils.getChunkKey(x, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // queues used for BFS propagating levels
++ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
++ }
++
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++
++ protected static final class LevelMap extends Long2ByteOpenHashMap {
++ public LevelMap() {
++ super();
++ }
++
++ public LevelMap(final int expected, final float loadFactor) {
++ super(expected, loadFactor);
++ }
++
++ // copied from superclass
++ private int find(final long k) {
++ if (k == 0L) {
++ return this.containsNullKey ? this.n : -(this.n + 1);
++ } else {
++ final long[] key = this.key;
++ long curr;
++ int pos;
++ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
++ return -(pos + 1);
++ } else if (k == curr) {
++ return pos;
++ } else {
++ while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
++ if (k == curr) {
++ return pos;
++ }
++ }
++
++ return -(pos + 1);
++ }
++ }
++ }
++
++ // copied from superclass
++ private void insert(final int pos, final long k, final byte v) {
++ if (pos == this.n) {
++ this.containsNullKey = true;
++ }
++
++ this.key[pos] = k;
++ this.value[pos] = v;
++ if (this.size++ >= this.maxFill) {
++ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++ }
++ }
++
++ // copied from superclass
++ public byte putIfGreater(final long key, final byte value) {
++ final int pos = this.find(key);
++ if (pos < 0) {
++ if (this.defRetValue < value) {
++ this.insert(-pos - 1, key, value);
++ }
++ return this.defRetValue;
++ } else {
++ final byte curr = this.value[pos];
++ if (value > curr) {
++ this.value[pos] = value;
++ return curr;
++ }
++ return curr;
++ }
++ }
++
++ // copied from superclass
++ private void removeEntry(final int pos) {
++ --this.size;
++ this.shiftKeys(pos);
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
++
++ // copied from superclass
++ private void removeNullEntry() {
++ this.containsNullKey = false;
++ --this.size;
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
++
++ // copied from superclass
++ public byte removeIfGreaterOrEqual(final long key, final byte value) {
++ if (key == 0L) {
++ if (!this.containsNullKey) {
++ return this.defRetValue;
++ }
++ final byte current = this.value[this.n];
++ if (value >= current) {
++ this.removeNullEntry();
++ return current;
++ }
++ return current;
++ } else {
++ long[] keys = this.key;
++ byte[] values = this.value;
++ long curr;
++ int pos;
++ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
++ return this.defRetValue;
++ } else if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ } else {
++ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
++ if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ }
++ }
++
++ return this.defRetValue;
++ }
++ }
++ }
++ }
++
++ protected static final class WorkQueue {
++
++ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
++ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
++
++ }
++
++ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public long removeFirstLong() {
++ // copied from superclass
++ long t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
++ }
++
++ return t;
++ }
++ }
++
++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public byte removeFirstByte() {
++ // copied from superclass
++ byte t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
++ }
++
++ return t;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c2d917c2eac55b8a4411a6e159f177f9428b1150
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
+@@ -0,0 +1,22 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import java.lang.invoke.VarHandle;
++
++public final class LazyRunnable implements Runnable {
++
++ private volatile Runnable toRun;
++ private static final VarHandle TO_RUN_HANDLE = ConcurrentUtil.getVarHandle(LazyRunnable.class, "toRun", Runnable.class);
++
++ public void setRunnable(final Runnable run) {
++ final Runnable prev = (Runnable)TO_RUN_HANDLE.compareAndExchange(this, (Runnable)null, run);
++ if (prev != null) {
++ throw new IllegalStateException("Runnable already set");
++ }
++ }
++
++ @Override
++ public void run() {
++ ((Runnable)TO_RUN_HANDLE.getVolatile(this)).run();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7e440b4a46b040365df7317035e577d93e7d855d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -0,0 +1,273 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.level.ChunkPos;
++import java.util.ArrayList;
++
++public final class NearbyPlayers {
++
++ public static enum NearbyMapType {
++ GENERAL,
++ GENERAL_SMALL,
++ GENERAL_REALLY_SMALL,
++ TICK_VIEW_DISTANCE,
++ VIEW_DISTANCE,
++ // Moonrise start - chunk tick iteration
++ SPAWN_RANGE {
++ @Override
++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
++ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ);
++ }
++
++ @Override
++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
++ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ);
++ }
++ };
++ // Moonrise end - chunk tick iteration
++
++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
++
++ }
++
++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
++
++ }
++ }
++
++ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
++ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length;
++
++ private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1;
++ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
++ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
++
++ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
++ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
++ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
++
++ private final ServerLevel world;
++ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
++ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
++ private final Long2ReferenceOpenHashMap<ReferenceList<ServerPlayer>>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES];
++ {
++ for (int i = 0; i < this.directByChunk.length; ++i) {
++ this.directByChunk[i] = new Long2ReferenceOpenHashMap<>();
++ }
++ }
++
++ public NearbyPlayers(final ServerLevel world) {
++ this.world = world;
++ }
++
++ public void addPlayer(final ServerPlayer player) {
++ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
++ if (this.players.putIfAbsent(player, newTrackers) != null) {
++ throw new IllegalStateException("Already have player " + player);
++ }
++
++ final ChunkPos chunk = player.chunkPosition();
++
++ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
++ // use 0 for default, will be updated by tickPlayer
++ (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0);
++ }
++
++ // update view distances
++ this.tickPlayer(player);
++ }
++
++ public void removePlayer(final ServerPlayer player) {
++ final TrackedPlayer[] players = this.players.remove(player);
++ if (players == null) {
++ return; // May be called during teleportation before the player is actually placed
++ }
++
++ for (final TrackedPlayer tracker : players) {
++ tracker.remove();
++ }
++ }
++
++ public void clear() {
++ if (this.players.isEmpty()) {
++ return;
++ }
++
++ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) {
++ this.removePlayer(player);
++ }
++ }
++
++ public void tickPlayer(final ServerPlayer player) {
++ final TrackedPlayer[] players = this.players.get(player);
++ if (players == null) {
++ throw new IllegalStateException("Don't have player " + player);
++ }
++
++ final ChunkPos chunk = player.chunkPosition();
++
++ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
++ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
++ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
++ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
++ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getViewDistance(player));
++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
++ }
++
++ public TrackedChunk getChunk(final ChunkPos pos) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++ }
++
++ public TrackedChunk getChunk(final BlockPos pos) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++ }
++
++ public TrackedChunk getChunk(final int chunkX, final int chunkZ) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
++ }
++
++ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
++ }
++
++ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
++ }
++
++ public static final class TrackedChunk {
++
++ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
++
++ private final long chunkKey;
++ private final NearbyPlayers nearbyPlayers;
++ private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
++ private int nonEmptyLists;
++ private long updateCount;
++
++ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) {
++ this.chunkKey = chunkKey;
++ this.nearbyPlayers = nearbyPlayers;
++ }
++
++ public boolean isEmpty() {
++ return this.nonEmptyLists == 0;
++ }
++
++ public long getUpdateCount() {
++ return this.updateCount;
++ }
++
++ public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
++ return this.players[type.ordinal()];
++ }
++
++ public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
++ ++this.updateCount;
++
++ final int idx = type.ordinal();
++ final ReferenceList<ServerPlayer> list = this.players[idx];
++ if (list == null) {
++ ++this.nonEmptyLists;
++ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY));
++ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players);
++ players.add(player);
++ return;
++ }
++
++ if (!list.add(player)) {
++ throw new IllegalStateException("Already contains player " + player);
++ }
++ }
++
++ public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
++ ++this.updateCount;
++
++ final int idx = type.ordinal();
++ final ReferenceList<ServerPlayer> list = this.players[idx];
++ if (list == null) {
++ throw new IllegalStateException("Does not contain player " + player);
++ }
++
++ if (!list.remove(player)) {
++ throw new IllegalStateException("Does not contain player " + player);
++ }
++
++ if (list.size() == 0) {
++ this.players[idx] = null;
++ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
++ --this.nonEmptyLists;
++ }
++ }
++ }
++
++ private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
++
++ private final NearbyMapType type;
++
++ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
++ super(player);
++ this.type = type;
++ }
++
++ @Override
++ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
++ final NearbyMapType type = this.type;
++ if (chunk != null) {
++ chunk.addPlayer(parameter, type);
++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
++ } else {
++ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this);
++ NearbyPlayers.this.byChunk.put(chunkKey, created);
++ created.addPlayer(parameter, type);
++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
++
++ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created;
++ }
++ }
++
++ @Override
++ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
++ if (chunk == null) {
++ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
++ }
++
++ final NearbyMapType type = this.type;
++ chunk.removePlayer(parameter, type);
++ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
++
++ if (chunk.isEmpty()) {
++ NearbyPlayers.this.byChunk.remove(chunkKey);
++ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey);
++ if (chunkData != null) {
++ chunkData.nearbyPlayers = null;
++ }
++ }
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..90560769d09538f7a740753a41a3b8e017b0b92a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+@@ -0,0 +1,99 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.IntPairUtil;
++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongSet;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceSet;
++
++public final class PositionCountingAreaMap<T> {
++
++ private final Reference2ReferenceOpenHashMap<T, PositionCounter> counters = new Reference2ReferenceOpenHashMap<>();
++ private final Long2IntOpenHashMap positions = new Long2IntOpenHashMap();
++
++ public ReferenceSet<T> getObjects() {
++ return this.counters.keySet();
++ }
++
++ public LongSet getPositions() {
++ return this.positions.keySet();
++ }
++
++ public int getTotalPositions() {
++ return this.positions.size();
++ }
++
++ public boolean hasObjectsNear(final int toX, final int toZ) {
++ return this.positions.containsKey(IntPairUtil.key(toX, toZ));
++ }
++
++ public int getObjectsNear(final int toX, final int toZ) {
++ return this.positions.get(IntPairUtil.key(toX, toZ));
++ }
++
++ public boolean add(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter existing = this.counters.get(parameter);
++ if (existing != null) {
++ return false;
++ }
++
++ final PositionCounter counter = new PositionCounter(parameter);
++
++ this.counters.put(parameter, counter);
++
++ return counter.add(toX, toZ, distance);
++ }
++
++ public boolean addOrUpdate(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter existing = this.counters.get(parameter);
++ if (existing != null) {
++ return existing.update(toX, toZ, distance);
++ }
++
++ final PositionCounter counter = new PositionCounter(parameter);
++
++ this.counters.put(parameter, counter);
++
++ return counter.add(toX, toZ, distance);
++ }
++
++ public boolean remove(final T parameter) {
++ final PositionCounter counter = this.counters.remove(parameter);
++ if (counter == null) {
++ return false;
++ }
++
++ counter.remove();
++
++ return true;
++ }
++
++ public boolean update(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter counter = this.counters.get(parameter);
++ if (counter == null) {
++ return false;
++ }
++
++ return counter.update(toX, toZ, distance);
++ }
++
++ private final class PositionCounter extends SingleUserAreaMap<T> {
++
++ public PositionCounter(final T parameter) {
++ super(parameter);
++ }
++
++ @Override
++ protected void addCallback(final T parameter, final int toX, final int toZ) {
++ PositionCountingAreaMap.this.positions.addTo(IntPairUtil.key(toX, toZ), 1);
++ }
++
++ @Override
++ protected void removeCallback(final T parameter, final int toX, final int toZ) {
++ final long key = IntPairUtil.key(toX, toZ);
++ if (PositionCountingAreaMap.this.positions.addTo(key, -1) == 1) {
++ PositionCountingAreaMap.this.positions.remove(key);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..94689e0342cf95dbedec955d67c95fa07a219678
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+@@ -0,0 +1,248 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++
++public abstract class SingleUserAreaMap<T> {
++
++ public static final int NOT_SET = Integer.MIN_VALUE;
++
++ private final T parameter;
++ private int lastChunkX = NOT_SET;
++ private int lastChunkZ = NOT_SET;
++ private int distance = NOT_SET;
++
++ public SingleUserAreaMap(final T parameter) {
++ this.parameter = parameter;
++ }
++
++ public final T getParameter() {
++ return this.parameter;
++ }
++
++ public final int getLastChunkX() {
++ return this.lastChunkX;
++ }
++
++ public final int getLastChunkZ() {
++ return this.lastChunkZ;
++ }
++
++ public final int getLastDistance() {
++ return this.distance;
++ }
++
++ /* math sign function except 0 returns 1 */
++ protected static int sign(int val) {
++ return 1 | (val >> (Integer.SIZE - 1));
++ }
++
++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
++
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.addCallback(parameter, cx, cz);
++ }
++ }
++ }
++
++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
++
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.removeCallback(parameter, cx, cz);
++ }
++ }
++ }
++
++ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
++ if (distance < 0) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ if (this.lastChunkX != NOT_SET) {
++ return false;
++ }
++ this.lastChunkX = chunkX;
++ this.lastChunkZ = chunkZ;
++ this.distance = distance;
++
++ this.addToNew(this.parameter, chunkX, chunkZ, distance);
++
++ return true;
++ }
++
++ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
++ if (newViewDistance < 0) {
++ throw new IllegalArgumentException(Integer.toString(newViewDistance));
++ }
++ final int fromX = this.lastChunkX;
++ final int fromZ = this.lastChunkZ;
++ final int oldViewDistance = this.distance;
++ if (fromX == NOT_SET) {
++ return false;
++ }
++
++ this.lastChunkX = toX;
++ this.lastChunkZ = toZ;
++ this.distance = newViewDistance;
++
++ final T parameter = this.parameter;
++
++
++ final int dx = toX - fromX;
++ final int dz = toZ - fromZ;
++
++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
++
++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
++ // teleported
++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
++ this.addToNew(parameter, toX, toZ, newViewDistance);
++ return true;
++ }
++
++ if (oldViewDistance != newViewDistance) {
++ // remove loop
++
++ final int oldMinX = fromX - oldViewDistance;
++ final int oldMinZ = fromZ - oldViewDistance;
++ final int oldMaxX = fromX + oldViewDistance;
++ final int oldMaxZ = fromZ + oldViewDistance;
++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
++
++ // only remove if we're outside the new view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ // add loop
++
++ final int newMinX = toX - newViewDistance;
++ final int newMinZ = toZ - newViewDistance;
++ final int newMaxX = toX + newViewDistance;
++ final int newMaxZ = toZ + newViewDistance;
++ for (int currX = newMinX; currX <= newMaxX; ++currX) {
++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
++
++ // only add if we're outside the old view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ return true;
++ }
++
++ // x axis is width
++ // z axis is height
++ // right refers to the x axis of where we moved
++ // top refers to the z axis of where we moved
++
++ // same view distance
++
++ // used for relative positioning
++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
++
++ // The area excluded by overlapping the two view distance squares creates four rectangles:
++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
++ // and on the right the "added" section.
++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
++ // exclusive to the regions they surround.
++
++ // 4 points of the rectangle
++ int maxX; // exclusive
++ int minX; // inclusive
++ int maxZ; // exclusive
++ int minZ; // inclusive
++
++ if (dx != 0) {
++ // handle right addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX + (oldViewDistance * right) + right; // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dz != 0) {
++ // handle up addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = toX - (oldViewDistance * right); // inclusive
++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dx != 0) {
++ // handle left removal
++
++ maxX = toX - (oldViewDistance * right); // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dz != 0) {
++ // handle down removal
++
++ maxX = fromX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = toZ - (oldViewDistance * up); // exclusive
++ minZ = fromZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ return true;
++ }
++
++ public final boolean remove() {
++ final int chunkX = this.lastChunkX;
++ final int chunkZ = this.lastChunkZ;
++ final int distance = this.distance;
++ if (chunkX == NOT_SET) {
++ return false;
++ }
++
++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
++
++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
++
++ return true;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
+@@ -0,0 +1,68 @@
++package ca.spottedleaf.moonrise.common.set;
++
++import java.util.Collection;
++
++public final class OptimizedSmallEnumSet<E extends Enum<E>> {
++
++ private final Class<E> enumClass;
++ private long backingSet;
++
++ public OptimizedSmallEnumSet(final Class<E> clazz) {
++ if (clazz == null) {
++ throw new IllegalArgumentException("Null class");
++ }
++ if (!clazz.isEnum()) {
++ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
++ }
++ this.enumClass = clazz;
++ }
++
++ public boolean addUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
++
++ final long prev = this.backingSet;
++ this.backingSet = prev | key;
++
++ return (prev & key) == 0;
++ }
++
++ public boolean removeUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
++
++ final long prev = this.backingSet;
++ this.backingSet = prev & ~key;
++
++ return (prev & key) != 0;
++ }
++
++ public void clear() {
++ this.backingSet = 0L;
++ }
++
++ public int size() {
++ return Long.bitCount(this.backingSet);
++ }
++
++ public void addAllUnchecked(final Collection<E> enums) {
++ for (final E element : enums) {
++ if (element == null) {
++ throw new NullPointerException("Null element");
++ }
++ this.backingSet |= (1L << element.ordinal());
++ }
++ }
++
++ public long getBackingSet() {
++ return this.backingSet;
++ }
++
++ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
++ return (other.backingSet & this.backingSet) != 0;
++ }
++
++ public boolean hasElement(final E element) {
++ return (this.backingSet & (1L << element.ordinal())) != 0;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..58a99bc38e137431f10af36fa9e2d04fe61694aa
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+@@ -0,0 +1,288 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++import com.mojang.logging.LogUtils;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import org.slf4j.Logger;
++import java.util.List;
++import java.util.function.Consumer;
++
++public final class ChunkSystem {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
++
++ private static int getDistance(final ChunkStatus status) {
++ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
++ }
++
++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
++ level.chunkSource.mainThreadProcessor.execute(run);
++ }
++
++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
++ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
++ final Consumer<ChunkAccess> onComplete) {
++ if (gen) {
++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ return;
++ }
++ scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
++ if (chunk == null) {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ } else {
++ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ } else {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ }
++ }
++ });
++ }
++
++ static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
++
++ private static long chunkLoadCounter = 0L;
++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
++ if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++ scheduleChunkTask(level, chunkX, chunkZ, () -> {
++ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }, priority);
++ return;
++ }
++
++ final int minLevel = 33 + getDistance(toStatus);
++ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
++ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
++
++ if (addTicket) {
++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++ }
++ level.chunkSource.runDistanceManagerUpdates();
++
++ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
++ try {
++ if (onComplete != null) {
++ onComplete.accept(chunk);
++ }
++ } catch (final Throwable thr) {
++ LOGGER.error("Exception handling chunk load callback", thr);
++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
++ } finally {
++ if (addTicket) {
++ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++ }
++ }
++ };
++
++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++ if (holder == null || holder.getTicketLevel() > minLevel) {
++ loadCallback.accept(null);
++ return;
++ }
++
++ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
++
++ if (loadFuture.isDone()) {
++ loadCallback.accept(loadFuture.join().orElse(null));
++ return;
++ }
++
++ loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
++ if (thr != null) {
++ loadCallback.accept(null);
++ return;
++ }
++ loadCallback.accept(result.orElse(null));
++ }, (final Runnable r) -> {
++ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++ });
++ }
++
++ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
++ final FullChunkStatus toStatus, final boolean addTicket,
++ final Priority priority, final Consumer<LevelChunk> onComplete) {
++ // This method goes unused until the chunk system rewrite
++ if (toStatus == FullChunkStatus.INACCESSIBLE) {
++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
++ }
++
++ if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++ scheduleChunkTask(level, chunkX, chunkZ, () -> {
++ scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }, priority);
++ return;
++ }
++
++ final int minLevel = 33 - (toStatus.ordinal() - 1);
++ final int radius = toStatus.ordinal() - 1;
++ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
++ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
++
++ if (addTicket) {
++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++ }
++ level.chunkSource.runDistanceManagerUpdates();
++
++ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
++ try {
++ if (onComplete != null) {
++ onComplete.accept(chunk);
++ }
++ } catch (final Throwable thr) {
++ LOGGER.error("Exception handling chunk load callback", thr);
++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
++ } finally {
++ if (addTicket) {
++ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++ }
++ }
++ };
++
++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++ if (holder == null || holder.getTicketLevel() > minLevel) {
++ loadCallback.accept(null);
++ return;
++ }
++
++ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
++ switch (toStatus) {
++ case FULL: {
++ tickingState = holder.getFullChunkFuture();
++ break;
++ }
++ case BLOCK_TICKING: {
++ tickingState = holder.getTickingChunkFuture();
++ break;
++ }
++ case ENTITY_TICKING: {
++ tickingState = holder.getEntityTickingChunkFuture();
++ break;
++ }
++ default: {
++ throw new IllegalStateException("Cannot reach here");
++ }
++ }
++
++ if (tickingState.isDone()) {
++ loadCallback.accept(tickingState.join().orElse(null));
++ return;
++ }
++
++ tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
++ if (thr != null) {
++ loadCallback.accept(null);
++ return;
++ }
++ loadCallback.accept(result.orElse(null));
++ }, (final Runnable r) -> {
++ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++ });
++ }
++
++ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
++ return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++ }
++
++ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
++ return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++ }
++
++ public static int getVisibleChunkHolderCount(final ServerLevel level) {
++ return level.chunkSource.chunkMap.visibleChunkMap.size();
++ }
++
++ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
++ return level.chunkSource.chunkMap.updatingChunkMap.size();
++ }
++
++ public static boolean hasAnyChunkHolders(final ServerLevel level) {
++ return getUpdatingChunkHolderCount(level) != 0;
++ }
++
++ public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) {
++ if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) {
++ return false;
++ }
++ return true;
++ }
++
++ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
++ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
++ }
++
++ public static int getSendViewDistance(final ServerPlayer player) {
++ return getViewDistance(player);
++ }
++
++ public static int getViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ if (level == null) {
++ return org.bukkit.Bukkit.getViewDistance();
++ }
++ return level.chunkSource.chunkMap.serverViewDistance;
++ }
++
++ public static int getTickViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ if (level == null) {
++ return org.bukkit.Bukkit.getSimulationDistance();
++ }
++ return level.chunkSource.chunkMap.distanceManager.simulationDistance;
++ }
++
++ private ChunkSystem() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..31b92bd48828cbea25b44a9f0f96886347aa1ae6
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.SectionPos;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.Vec3;
++
++public final class CoordinateUtils {
++
++ // the chunk keys are compatible with vanilla
++
++ public static long getChunkKey(final BlockPos pos) {
++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final ChunkPos pos) {
++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final SectionPos pos) {
++ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final int x, final int z) {
++ return ((long)z << 32) | (x & 0xFFFFFFFFL);
++ }
++
++ public static int getChunkX(final long chunkKey) {
++ return (int)chunkKey;
++ }
++
++ public static int getChunkZ(final long chunkKey) {
++ return (int)(chunkKey >>> 32);
++ }
++
++ public static int getChunkCoordinate(final double blockCoordinate) {
++ return Mth.floor(blockCoordinate) >> 4;
++ }
++
++ // the section keys are compatible with vanilla's
++
++ static final int SECTION_X_BITS = 22;
++ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
++ static final int SECTION_Y_BITS = 20;
++ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
++ static final int SECTION_Z_BITS = 22;
++ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
++ // format is y,z,x (in order of LSB to MSB)
++ static final int SECTION_Y_SHIFT = 0;
++ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
++ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
++ static final int SECTION_TO_BLOCK_SHIFT = 4;
++
++ public static long getChunkSectionKey(final int x, final int y, final int z) {
++ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final SectionPos pos) {
++ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
++ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final BlockPos pos) {
++ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
++ }
++
++ public static long getChunkSectionKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
++ }
++
++ public static int getChunkSectionX(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
++ }
++
++ public static int getChunkSectionY(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
++ }
++
++ public static int getChunkSectionZ(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
++ }
++
++ public static int getBlockX(final Vec3 pos) {
++ return Mth.floor(pos.x);
++ }
++
++ public static int getBlockY(final Vec3 pos) {
++ return Mth.floor(pos.y);
++ }
++
++ public static int getBlockZ(final Vec3 pos) {
++ return Mth.floor(pos.z);
++ }
++
++ public static int getChunkX(final Vec3 pos) {
++ return Mth.floor(pos.x) >> 4;
++ }
++
++ public static int getChunkY(final Vec3 pos) {
++ return Mth.floor(pos.y) >> 4;
++ }
++
++ public static int getChunkZ(final Vec3 pos) {
++ return Mth.floor(pos.z) >> 4;
++ }
++
++ private CoordinateUtils() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0531f25aaad162386a029d33e68d7c8336b9d5d1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
+@@ -0,0 +1,109 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import java.util.Objects;
++
++public final class FlatBitsetUtil {
++
++ private static final int LOG2_LONG = 6;
++ private static final long ALL_SET = -1L;
++ private static final int BITS_PER_LONG = Long.SIZE;
++
++ // from inclusive
++ // to exclusive
++ public static int firstSet(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = bitset[++bitsetIdx];
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static int firstClear(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++ // like firstSet, but invert the bitset
++
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = ~bitset[++bitsetIdx];
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static void clearRange(final long[] bitset, final int from, int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++
++ if (from == to) {
++ return;
++ }
++
++ --to;
++
++ final int fromBitsetIdx = from >>> LOG2_LONG;
++ final int toBitsetIdx = to >>> LOG2_LONG;
++
++ final long keepFirst = ~(ALL_SET << from);
++ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
++
++ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
++
++ if (fromBitsetIdx == toBitsetIdx) {
++ // special case: need to keep both first and last
++ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
++ } else {
++ bitset[fromBitsetIdx] &= keepFirst;
++
++ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
++ bitset[i] = 0L;
++ }
++
++ bitset[toBitsetIdx] &= keepLast;
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
++ return firstClear(bitset, from, to) == -1;
++ }
++
++
++ private FlatBitsetUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..91efda726b87a8a8f28dee84e31b6a7063752ebd
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+@@ -0,0 +1,34 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import com.google.gson.JsonElement;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import java.io.File;
++import java.io.FileOutputStream;
++import java.io.IOException;
++import java.io.PrintStream;
++import java.io.StringWriter;
++import java.nio.charset.StandardCharsets;
++
++public final class JsonUtil {
++
++ public static void writeJson(final JsonElement element, final File file) throws IOException {
++ final StringWriter stringWriter = new StringWriter();
++ final JsonWriter jsonWriter = new JsonWriter(stringWriter);
++ jsonWriter.setIndent(" ");
++ jsonWriter.setLenient(false);
++ Streams.write(element, jsonWriter);
++
++ final String jsonString = stringWriter.toString();
++
++ final File parent = file.getParentFile();
++ if (parent != null) {
++ parent.mkdirs();
++ }
++ file.createNewFile();
++ try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
++ out.print(jsonString);
++ }
++ }
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..97848869df61648fc415e4d39f409f433202c274
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -0,0 +1,14 @@
++package ca.spottedleaf.moonrise.common.util;
++
++public final class MixinWorkarounds {
++
++ // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++ // https://github.com/FabricMC/Mixin/pull/147
++ public static long[] clone(final long[] values) {
++ return values.clone();
++ }
++
++ public static byte[] clone(final byte[] values) {
++ return values.clone();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..632920e04686d8a0fd0a60e87348be1fe7862a3c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -0,0 +1,101 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool;
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++import com.mojang.logging.LogUtils;
++import org.slf4j.Logger;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.function.Consumer;
++
++public final class MoonriseCommon {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ public static final PrioritisedThreadPool WORKER_POOL = new PrioritisedThreadPool(
++ new Consumer<>() {
++ private final AtomicInteger idGenerator = new AtomicInteger();
++
++ @Override
++ public void accept(Thread thread) {
++ thread.setDaemon(true);
++ thread.setName(PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement());
++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++ @Override
++ public void uncaughtException(final Thread thread, final Throwable throwable) {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ }
++ });
++ }
++ }
++ );
++ public static final long WORKER_QUEUE_HOLD_TIME = (long)(20.0e6); // 20ms
++ public static final int CLIENT_DIVISION = 0;
++ public static final PrioritisedThreadPool.ExecutorGroup RENDER_EXECUTOR_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
++ public static final int SERVER_DIVISION = 1;
++ public static final PrioritisedThreadPool.ExecutorGroup PARALLEL_GEN_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup RADIUS_AWARE_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup LOAD_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++
++ public static void adjustWorkerThreads(final int configWorkerThreads, final int configIoThreads) {
++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
++ if (defaultWorkerThreads <= 4) {
++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
++ } else {
++ defaultWorkerThreads = defaultWorkerThreads / 2;
++ }
++ defaultWorkerThreads = Integer.getInteger(PlatformHooks.get().getBrand() + ".WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
++
++ int workerThreads = configWorkerThreads;
++
++ if (workerThreads <= 0) {
++ workerThreads = defaultWorkerThreads;
++ }
++
++ final int ioThreads = Math.max(1, configIoThreads);
++
++ WORKER_POOL.adjustThreadCount(workerThreads);
++ IO_POOL.adjustThreadCount(ioThreads);
++
++ LOGGER.info(PlatformHooks.get().getBrand() + " is using " + workerThreads + " worker threads, " + ioThreads + " I/O threads");
++ }
++
++ public static final PrioritisedThreadPool IO_POOL = new PrioritisedThreadPool(
++ new Consumer<>() {
++ private final AtomicInteger idGenerator = new AtomicInteger();
++
++ @Override
++ public void accept(final Thread thread) {
++ thread.setDaemon(true);
++ thread.setName(PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement());
++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++ @Override
++ public void uncaughtException(final Thread thread, final Throwable throwable) {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ }
++ });
++ }
++ }
++ );
++ public static final long IO_QUEUE_HOLD_TIME = (long)(100.0e6); // 100ms
++ public static final PrioritisedThreadPool.ExecutorGroup CLIENT_PROFILER_IO_GROUP = IO_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup SERVER_REGION_IO_GROUP = IO_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++
++ public static void haltExecutors() {
++ MoonriseCommon.WORKER_POOL.shutdown(false);
++ LOGGER.info("Awaiting termination of worker pool for up to 60s...");
++ if (!MoonriseCommon.WORKER_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
++ LOGGER.error("Worker pool did not shut down in time!");
++ MoonriseCommon.WORKER_POOL.halt(false);
++ }
++
++ MoonriseCommon.IO_POOL.shutdown(false);
++ LOGGER.info("Awaiting termination of I/O pool for up to 60s...");
++ if (!MoonriseCommon.IO_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
++ LOGGER.error("I/O pool did not shut down in time!");
++ MoonriseCommon.IO_POOL.halt(false);
++ }
++ }
++
++ private MoonriseCommon() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..559c959aff3c9deef867b9e425fba3e2e669cac6
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -0,0 +1,11 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++
++public final class MoonriseConstants {
++
++ public static final int MAX_VIEW_DISTANCE = Integer.getInteger(PlatformHooks.get().getBrand() + ".MaxViewDistance", 32);
++
++ private MoonriseConstants() {}
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleThreadUnsafeRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleThreadUnsafeRandom.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8d57b9c141fbe049aea248faa547dc97ba24cba5
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleThreadUnsafeRandom.java
+@@ -0,0 +1,105 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.util.Mth;
++import net.minecraft.util.RandomSource;
++import net.minecraft.world.level.levelgen.BitRandomSource;
++import net.minecraft.world.level.levelgen.MarsagliaPolarGaussian;
++import net.minecraft.world.level.levelgen.PositionalRandomFactory;
++
++/**
++ * Avoid costly CAS of superclass + division in nextInt
++ */
++public final class SimpleThreadUnsafeRandom implements BitRandomSource {
++
++ private static final long MULTIPLIER = 25214903917L;
++ private static final long ADDEND = 11L;
++ private static final int BITS = 48;
++ private static final long MASK = (1L << BITS) - 1L;
++
++ private long value;
++ private final MarsagliaPolarGaussian gaussianSource = new MarsagliaPolarGaussian(this);
++
++ public SimpleThreadUnsafeRandom(final long seed) {
++ this.setSeed(seed);
++ }
++
++ @Override
++ public void setSeed(final long seed) {
++ this.value = (seed ^ MULTIPLIER) & MASK;
++ this.gaussianSource.reset();
++ }
++
++ private long advanceSeed() {
++ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK;
++ }
++
++ @Override
++ public int next(final int bits) {
++ return (int)(this.advanceSeed() >>> (BITS - bits));
++ }
++
++ @Override
++ public int nextInt() {
++ final long seed = this.advanceSeed();
++ return (int)(seed >>> (BITS - Integer.SIZE));
++ }
++
++ @Override
++ public int nextInt(final int bound) {
++ if (bound <= 0) {
++ throw new IllegalArgumentException();
++ }
++
++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
++ final long value = this.advanceSeed() >>> (BITS - Integer.SIZE);
++ return (int)((value * (long)bound) >>> Integer.SIZE);
++ }
++
++ @Override
++ public double nextGaussian() {
++ return this.gaussianSource.nextGaussian();
++ }
++
++ @Override
++ public RandomSource fork() {
++ return new SimpleThreadUnsafeRandom(this.nextLong());
++ }
++
++ @Override
++ public PositionalRandomFactory forkPositional() {
++ return new SimpleRandomPositionalFactory(this.nextLong());
++ }
++
++ public static final class SimpleRandomPositionalFactory implements PositionalRandomFactory {
++
++ private final long seed;
++
++ public SimpleRandomPositionalFactory(final long seed) {
++ this.seed = seed;
++ }
++
++ public long getSeed() {
++ return this.seed;
++ }
++
++ @Override
++ public RandomSource fromHashOf(final String string) {
++ return new SimpleThreadUnsafeRandom((long)string.hashCode() ^ this.seed);
++ }
++
++ @Override
++ public RandomSource fromSeed(final long seed) {
++ return new SimpleThreadUnsafeRandom(seed);
++ }
++
++ @Override
++ public RandomSource at(final int x, final int y, final int z) {
++ return new SimpleThreadUnsafeRandom(Mth.getSeed(x, y, z) ^ this.seed);
++ }
++
++ @Override
++ public void parityConfigString(final StringBuilder stringBuilder) {
++ stringBuilder.append("SimpleRandomPositionalFactory{").append(this.seed).append('}');
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..12eb3add0931a4d77acdf6e875c42dda9c313dc3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
+@@ -0,0 +1,94 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.util.Mth;
++import net.minecraft.util.RandomSource;
++import net.minecraft.world.level.levelgen.BitRandomSource;
++import net.minecraft.world.level.levelgen.MarsagliaPolarGaussian;
++import net.minecraft.world.level.levelgen.PositionalRandomFactory;
++
++/**
++ * Avoid costly CAS of superclass
++ */
++public final class ThreadUnsafeRandom implements BitRandomSource {
++
++ private static final long MULTIPLIER = 25214903917L;
++ private static final long ADDEND = 11L;
++ private static final int BITS = 48;
++ private static final long MASK = (1L << BITS) - 1L;
++
++ private long value;
++ private final MarsagliaPolarGaussian gaussianSource = new MarsagliaPolarGaussian(this);
++
++ public ThreadUnsafeRandom(final long seed) {
++ this.setSeed(seed);
++ }
++
++ @Override
++ public void setSeed(final long seed) {
++ this.value = (seed ^ MULTIPLIER) & MASK;
++ this.gaussianSource.reset();
++ }
++
++ private long advanceSeed() {
++ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK;
++ }
++
++ @Override
++ public int next(final int bits) {
++ return (int)(this.advanceSeed() >>> (BITS - bits));
++ }
++
++ @Override
++ public int nextInt() {
++ final long seed = this.advanceSeed();
++ return (int)(seed >>> (BITS - Integer.SIZE));
++ }
++
++ @Override
++ public double nextGaussian() {
++ return this.gaussianSource.nextGaussian();
++ }
++
++ @Override
++ public RandomSource fork() {
++ return new ThreadUnsafeRandom(this.nextLong());
++ }
++
++ @Override
++ public PositionalRandomFactory forkPositional() {
++ return new ThreadUnsafeRandomPositionalFactory(this.nextLong());
++ }
++
++ public static final class ThreadUnsafeRandomPositionalFactory implements PositionalRandomFactory {
++
++ private final long seed;
++
++ public ThreadUnsafeRandomPositionalFactory(final long seed) {
++ this.seed = seed;
++ }
++
++ public long getSeed() {
++ return this.seed;
++ }
++
++ @Override
++ public RandomSource fromHashOf(final String string) {
++ return new ThreadUnsafeRandom((long)string.hashCode() ^ this.seed);
++ }
++
++ @Override
++ public RandomSource fromSeed(final long seed) {
++ return new ThreadUnsafeRandom(seed);
++ }
++
++ @Override
++ public RandomSource at(final int x, final int y, final int z) {
++ return new ThreadUnsafeRandom(Mth.getSeed(x, y, z) ^ this.seed);
++ }
++
++ @Override
++ public void parityConfigString(final StringBuilder stringBuilder) {
++ stringBuilder.append("ThreadUnsafeRandomPositionalFactory{").append(this.seed).append('}');
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..217d1f908a36a5177ba3cbb80a33f73d4dab0fa0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -0,0 +1,143 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.phys.AABB;
++import net.minecraft.world.phys.Vec3;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++
++import java.util.concurrent.atomic.AtomicInteger;
++
++public class TickThread extends Thread {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class);
++
++ /**
++ * @deprecated
++ */
++ @Deprecated
++ public static void ensureTickThread(final String reason) {
++ if (!isTickThread()) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) {
++ if (!isTickThreadFor(world, chunkX, chunkZ)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Entity entity, final String reason) {
++ if (!isTickThreadFor(entity)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Level world, final AABB aabb, final String reason) {
++ if (!isTickThreadFor(world, aabb)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) {
++ if (!isTickThreadFor(world, blockX, blockZ)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
++
++ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
++
++ public TickThread(final String name) {
++ this(null, name);
++ }
++
++ public TickThread(final Runnable run, final String name) {
++ this(null, run, name);
++ }
++
++ public TickThread(final ThreadGroup group, final Runnable run, final String name) {
++ this(group, run, name, ID_GENERATOR.incrementAndGet());
++ }
++
++ private TickThread(final ThreadGroup group, final Runnable run, final String name, final int id) {
++ super(group, run, name);
++ this.id = id;
++ }
++
++ public static TickThread getCurrentTickThread() {
++ return (TickThread)Thread.currentThread();
++ }
++
++ public static boolean isTickThread() {
++ return Thread.currentThread() instanceof TickThread;
++ }
++
++ public static boolean isShutdownThread() {
++ return false;
++ }
++
++ public static boolean isTickThreadFor(final Level world, final BlockPos pos) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final ChunkPos pos) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final Vec3 pos) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final AABB aabb) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final Entity entity) {
++ return isTickThread();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..efda2688ae1254a82ba7f6bf8bf597ef224cbb86
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -0,0 +1,62 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelHeightAccessor;
++
++public final class WorldUtil {
++
++ // min, max are inclusive
++
++ public static int getMaxSection(final LevelHeightAccessor world) {
++ return world.getMaxSectionY();
++ }
++
++ public static int getMaxSection(final Level world) {
++ return world.getMaxSectionY();
++ }
++
++ public static int getMinSection(final LevelHeightAccessor world) {
++ return world.getMinSectionY();
++ }
++
++ public static int getMinSection(final Level world) {
++ return world.getMinSectionY();
++ }
++
++ public static int getMaxLightSection(final LevelHeightAccessor world) {
++ return getMaxSection(world) + 1;
++ }
++
++ public static int getMinLightSection(final LevelHeightAccessor world) {
++ return getMinSection(world) - 1;
++ }
++
++
++
++ public static int getTotalSections(final LevelHeightAccessor world) {
++ return getMaxSection(world) - getMinSection(world) + 1;
++ }
++
++ public static int getTotalLightSections(final LevelHeightAccessor world) {
++ return getMaxLightSection(world) - getMinLightSection(world) + 1;
++ }
++
++ public static int getMinBlockY(final LevelHeightAccessor world) {
++ return getMinSection(world) << 4;
++ }
++
++ public static int getMaxBlockY(final LevelHeightAccessor world) {
++ return (getMaxSection(world) << 4) | 15;
++ }
++
++ public static String getWorldName(final Level world) {
++ if (world == null) {
++ return "null world";
++ }
++ return world.getWorld().getName(); // Paper
++ }
++
++ private WorldUtil() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1aa6be257ce594d7a69fdff008cd29014a04fd75
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
+@@ -0,0 +1,209 @@
++package ca.spottedleaf.moonrise.paper;
++
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++import com.mojang.datafixers.DSL;
++import com.mojang.datafixers.DataFixer;
++import com.mojang.serialization.Dynamic;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.function.Predicate;
++
++public final class PaperHooks implements PlatformHooks {
++
++ @Override
++ public String getBrand() {
++ return "Paper";
++ }
++
++ @Override
++ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
++ return blockState.getLightEmission();
++ }
++
++ @Override
++ public Predicate<BlockState> maybeHasLightEmission() {
++ return (final BlockState state) -> {
++ return state.getLightEmission() != 0;
++ };
++ }
++
++ @Override
++ public boolean hasCurrentlyLoadingChunk() {
++ return false;
++ }
++
++ @Override
++ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
++ return null;
++ }
++
++ @Override
++ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
++
++ }
++
++ @Override
++ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
++
++ }
++
++ @Override
++ public boolean allowAsyncTicketUpdates() {
++ return true;
++ }
++
++ @Override
++ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
++
++ }
++
++ @Override
++ public void chunkUnloadFromWorld(final LevelChunk chunk) {
++
++ }
++
++ @Override
++ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
++
++ }
++
++ @Override
++ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
++
++ }
++
++ @Override
++ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
++
++ }
++
++ @Override
++ public void entityMove(final Entity entity, final long oldSection, final long newSection) {
++
++ }
++
++ @Override
++ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
++ return true;
++ }
++
++ @Override
++ public boolean configFixMC224294() {
++ return true;
++ }
++
++ @Override
++ public boolean configAutoConfigSendDistance() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance;
++ }
++
++ @Override
++ public double configPlayerMaxLoadRate() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
++ }
++
++ @Override
++ public double configPlayerMaxGenRate() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
++ }
++
++ @Override
++ public double configPlayerMaxSendRate() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
++ }
++
++ @Override
++ public int configPlayerMaxConcurrentLoads() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
++ }
++
++ @Override
++ public int configPlayerMaxConcurrentGens() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
++ }
++
++ @Override
++ public long configAutoSaveInterval(final ServerLevel world) {
++ return world.paperConfig().chunks.autoSaveInterval.value();
++ }
++
++ @Override
++ public int configMaxAutoSavePerTick(final ServerLevel world) {
++ return world.paperConfig().chunks.maxAutoSaveChunksPerTick;
++ }
++
++ @Override
++ public boolean configFixMC159283() {
++ return true;
++ }
++
++ @Override
++ public boolean forceNoSave(final ChunkAccess chunk) {
++ return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
++ }
++
++ @Override
++ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
++ final int fromVersion, final int toVersion) {
++ return (CompoundTag)dataFixer.update(
++ type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
++ ).getValue();
++ }
++
++ @Override
++ public boolean hasMainChunkLoadHook() {
++ return false;
++ }
++
++ @Override
++ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
++
++ }
++
++ @Override
++ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
++ return entities;
++ }
++
++ @Override
++ public void unloadEntity(final Entity entity) {
++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
++ }
++
++ @Override
++ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
++ net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
++ }
++
++ @Override
++ public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
++ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
++ }
++}
+diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
+index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644
+--- a/src/main/java/com/mojang/logging/LogUtils.java
++++ b/src/main/java/com/mojang/logging/LogUtils.java
+@@ -61,4 +61,9 @@ public class LogUtils {
+ public static Logger getLogger() {
+ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
+ }
++ // Paper start
++ public static Logger getClassLogger() {
++ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+index 87da4ff63294735bfcbfa8442fb8ae7196b0f197..2eb155d3df2e34c050fd28c5a64015e6e1232851 100644
+--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+@@ -215,7 +215,7 @@ public class GlobalConfiguration extends ConfigurationPart {
+
+ @PostProcess
+ private void postProcess() {
+-
++ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads);
+ }
+ }
+
+diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..197224e31175252d8438a8df585bbb65f2288d7f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
+@@ -0,0 +1,129 @@
++package io.papermc.paper.util;
++
++public final class IntervalledCounter {
++
++ private static final int INITIAL_SIZE = 8;
++
++ protected long[] times;
++ protected long[] counts;
++ protected final long interval;
++ protected long minTime;
++ protected long sum;
++ protected int head; // inclusive
++ protected int tail; // exclusive
++
++ public IntervalledCounter(final long interval) {
++ this.times = new long[INITIAL_SIZE];
++ this.counts = new long[INITIAL_SIZE];
++ this.interval = interval;
++ }
++
++ public void updateCurrentTime() {
++ this.updateCurrentTime(System.nanoTime());
++ }
++
++ public void updateCurrentTime(final long currentTime) {
++ long sum = this.sum;
++ int head = this.head;
++ final int tail = this.tail;
++ final long minTime = currentTime - this.interval;
++
++ final int arrayLen = this.times.length;
++
++ // guard against overflow by using subtraction
++ while (head != tail && this.times[head] - minTime < 0) {
++ sum -= this.counts[head];
++ // there are two ways we can do this:
++ // 1. free the count when adding
++ // 2. free it now
++ // option #2
++ this.counts[head] = 0;
++ if (++head >= arrayLen) {
++ head = 0;
++ }
++ }
++
++ this.sum = sum;
++ this.head = head;
++ this.minTime = minTime;
++ }
++
++ public void addTime(final long currTime) {
++ this.addTime(currTime, 1L);
++ }
++
++ public void addTime(final long currTime, final long count) {
++ // guard against overflow by using subtraction
++ if (currTime - this.minTime < 0) {
++ return;
++ }
++ int nextTail = (this.tail + 1) % this.times.length;
++ if (nextTail == this.head) {
++ this.resize();
++ nextTail = (this.tail + 1) % this.times.length;
++ }
++
++ this.times[this.tail] = currTime;
++ this.counts[this.tail] += count;
++ this.sum += count;
++ this.tail = nextTail;
++ }
++
++ public void updateAndAdd(final long count) {
++ final long currTime = System.nanoTime();
++ this.updateCurrentTime(currTime);
++ this.addTime(currTime, count);
++ }
++
++ public void updateAndAdd(final long count, final long currTime) {
++ this.updateCurrentTime(currTime);
++ this.addTime(currTime, count);
++ }
++
++ private void resize() {
++ final long[] oldElements = this.times;
++ final long[] oldCounts = this.counts;
++ final long[] newElements = new long[this.times.length * 2];
++ final long[] newCounts = new long[this.times.length * 2];
++ this.times = newElements;
++ this.counts = newCounts;
++
++ final int head = this.head;
++ final int tail = this.tail;
++ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
++ this.head = 0;
++ this.tail = size;
++
++ if (tail >= head) {
++ // sequentially ordered from [head, tail)
++ System.arraycopy(oldElements, head, newElements, 0, size);
++ System.arraycopy(oldCounts, head, newCounts, 0, size);
++ } else {
++ // ordered from [head, length)
++ // then followed by [0, tail)
++
++ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
++ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
++
++ System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head);
++ System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail);
++ }
++ }
++
++ // returns in units per second
++ public double getRate() {
++ return (double)this.sum / ((double)this.interval * 1.0E-9);
++ }
++
++ public long getInterval() {
++ return this.interval;
++ }
++
++ public long getSum() {
++ return this.sum;
++ }
++
++ public int totalDataPoints() {
++ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a4ac34ebb58a404f4fca7e763e61d4ab05ee3af4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/MCUtil.java
+@@ -0,0 +1,205 @@
++package io.papermc.paper.util;
++
++import com.google.common.collect.Collections2;
++import com.google.common.collect.Lists;
++import com.google.common.util.concurrent.ThreadFactoryBuilder;
++import io.papermc.paper.math.BlockPosition;
++import io.papermc.paper.math.FinePosition;
++import io.papermc.paper.math.Position;
++import java.util.Collection;
++import java.util.Collections;
++import java.util.List;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.ExecutionException;
++import java.util.concurrent.ExecutorService;
++import java.util.concurrent.Executors;
++import java.util.function.BiConsumer;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.Supplier;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Vec3i;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.phys.Vec3;
++import org.bukkit.Location;
++import org.bukkit.NamespacedKey;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++import org.bukkit.craftbukkit.util.Waitable;
++
++public final class MCUtil {
++ public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> {
++ if (!isMainThread()) {
++ MinecraftServer.getServer().execute(run);
++ } else {
++ run.run();
++ }
++ };
++ public static final ExecutorService ASYNC_EXECUTOR = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder()
++ .setNameFormat("Paper Async Task Handler Thread - %1$d")
++ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER))
++ .build()
++ );
++
++ private MCUtil() {
++ }
++
++ public static List<ChunkPos> getSpiralOutChunks(BlockPos blockposition, int radius) {
++ List<ChunkPos> list = com.google.common.collect.Lists.newArrayList();
++
++ list.add(new ChunkPos(blockposition.getX() >> 4, blockposition.getZ() >> 4));
++ for (int r = 1; r <= radius; r++) {
++ int x = -r;
++ int z = r;
++
++ // Iterates the edge of half of the box; then negates for other half.
++ while (x <= r && z > -r) {
++ list.add(new ChunkPos((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4));
++ list.add(new ChunkPos((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4));
++
++ if (x < r) {
++ x++;
++ } else {
++ z--;
++ }
++ }
++ }
++ return list;
++ }
++
++ public static <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
++ return future.thenApplyAsync(r -> r, MAIN_EXECUTOR);
++ }
++
++ public static <T> void thenOnMain(CompletableFuture<T> future, Consumer<T> consumer) {
++ future.thenAcceptAsync(consumer, MAIN_EXECUTOR);
++ }
++
++ public static <T> void thenOnMain(CompletableFuture<T> future, BiConsumer<T, Throwable> consumer) {
++ future.whenCompleteAsync(consumer, MAIN_EXECUTOR);
++ }
++
++ public static boolean isMainThread() {
++ return MinecraftServer.getServer().isSameThread();
++ }
++
++ public static void ensureMain(Runnable run) {
++ ensureMain(null, run);
++ }
++
++ /**
++ * Ensures the target code is running on the main thread.
++ */
++ public static void ensureMain(String reason, Runnable run) {
++ if (!isMainThread()) {
++ if (reason != null) {
++ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException());
++ }
++ MinecraftServer.getServer().processQueue.add(run);
++ return;
++ }
++ run.run();
++ }
++
++ public static <T> T ensureMain(Supplier<T> run) {
++ return ensureMain(null, run);
++ }
++
++ /**
++ * Ensures the target code is running on the main thread.
++ */
++ public static <T> T ensureMain(String reason, Supplier<T> run) {
++ if (!isMainThread()) {
++ if (reason != null) {
++ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "! Blocking thread until it returns ", new IllegalStateException());
++ }
++ Waitable<T> wait = new Waitable<>() {
++ @Override
++ protected T evaluate() {
++ return run.get();
++ }
++ };
++ MinecraftServer.getServer().processQueue.add(wait);
++ try {
++ return wait.get();
++ } catch (InterruptedException | ExecutionException e) {
++ MinecraftServer.LOGGER.warn("Encountered exception", e);
++ }
++ return null;
++ }
++ return run.get();
++ }
++
++ public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) {
++ return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2));
++ }
++
++ public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) {
++ return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);
++ }
++
++ public static Location toLocation(Level world, double x, double y, double z) {
++ return new Location(world.getWorld(), x, y, z);
++ }
++
++ public static Location toLocation(Level world, BlockPos pos) {
++ return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
++ }
++
++ public static BlockPos toBlockPosition(Location loc) {
++ return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
++ }
++
++ public static BlockPos toBlockPos(Position pos) {
++ return new BlockPos(pos.blockX(), pos.blockY(), pos.blockZ());
++ }
++
++ public static FinePosition toPosition(Vec3 vector) {
++ return Position.fine(vector.x, vector.y, vector.z);
++ }
++
++ public static BlockPosition toPosition(Vec3i vector) {
++ return Position.block(vector.getX(), vector.getY(), vector.getZ());
++ }
++
++ public static Vec3 toVec3(Position position) {
++ return new Vec3(position.x(), position.y(), position.z());
++ }
++
++ public static boolean isEdgeOfChunk(BlockPos pos) {
++ final int modX = pos.getX() & 15;
++ final int modZ = pos.getZ() & 15;
++ return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15);
++ }
++
++ public static void scheduleAsyncTask(Runnable run) {
++ ASYNC_EXECUTOR.execute(run);
++ }
++
++ public static <T> ResourceKey<T> toResourceKey(
++ final ResourceKey<? extends net.minecraft.core.Registry<T>> registry,
++ final NamespacedKey namespacedKey
++ ) {
++ return ResourceKey.create(registry, CraftNamespacedKey.toMinecraft(namespacedKey));
++ }
++
++ public static NamespacedKey fromResourceKey(final ResourceKey<?> key) {
++ return CraftNamespacedKey.fromMinecraft(key.location());
++ }
++
++ public static <A, M> List<A> transformUnmodifiable(final List<? extends M> nms, final Function<? super M, ? extends A> converter) {
++ return Collections.unmodifiableList(Lists.transform(nms, converter::apply));
++ }
++
++ public static <A, M> Collection<A> transformUnmodifiable(final Collection<? extends M> nms, final Function<? super M, ? extends A> converter) {
++ return Collections.unmodifiableCollection(Collections2.transform(nms, converter::apply));
++ }
++
++ public static <A, M, C extends Collection<M>> void addAndConvert(final C target, final Collection<A> toAdd, final Function<? super A, ? extends M> converter) {
++ for (final A value : toAdd) {
++ target.add(converter.apply(value));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9f74ee1a6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+@@ -0,0 +1,24 @@
++package io.papermc.paper.util;
++
++import org.bukkit.plugin.java.JavaPlugin;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.Optional;
++
++public class StackWalkerUtil {
++
++ @Nullable
++ public static JavaPlugin getFirstPluginCaller() {
++ Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
++ .walk(stream -> stream
++ .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
++ .map((frame) -> {
++ PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
++ return classLoader.getPlugin();
++ })
++ .findFirst());
++
++ return foundFrame.orElse(null);
++ }
++}
+diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
+index 56947030e423bf314f32c8dba7e841949336b8cf..a88cfb92dea57d2f9abc029cea94a1b921f66766 100644
+--- a/src/main/java/net/minecraft/Util.java
++++ b/src/main/java/net/minecraft/Util.java
+@@ -136,7 +136,7 @@ public class Util {
+ }
+
+ public static long getNanos() {
+- return timeSource.getAsLong();
++ return System.nanoTime(); // Paper
+ }
+
+ public static long getEpochMillis() {
+diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java
+index 6b588a4e639da11edeb933ec2bc4afde8f0b47f1..d721ae6d9b54cbace5b7ade657e9739fc7c42d14 100644
+--- a/src/main/java/net/minecraft/nbt/CompoundTag.java
++++ b/src/main/java/net/minecraft/nbt/CompoundTag.java
+@@ -235,6 +235,10 @@ public class CompoundTag implements Tag {
+ this.tags.put(key, NbtUtils.createUUID(value));
+ }
+
++
++ /**
++ * You must use {@link #hasUUID(String)} before or else it <b>will</b> throw an NPE.
++ */
+ public UUID getUUID(String key) {
+ return NbtUtils.loadUUID(this.get(key));
+ }
+diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
+index 28600c024f39961fde4d202724289741d779a992..1fc859f4cc1cf552d2d578b3cda5872bc8b1015a 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -121,6 +121,18 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+ BandwidthDebugMonitor bandwidthDebugMonitor;
+ public String hostname = ""; // CraftBukkit - add field
+
++ // Paper start - add utility methods
++ public final net.minecraft.server.level.ServerPlayer getPlayer() {
++ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
++ return impl.player;
++ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
++ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
++ return player == null ? null : player.getHandle();
++ }
++ return null;
++ }
++ // Paper end - add utility methods
++
+ public Connection(PacketFlow side) {
+ this.receiving = side;
+ }
+diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java
+index e82263b4b48c4544a5ade6613ea284f4de2b4c80..a58f67973b4ed986065860263c7a42214640520d 100644
+--- a/src/main/java/net/minecraft/network/PacketEncoder.java
++++ b/src/main/java/net/minecraft/network/PacketEncoder.java
+@@ -31,7 +31,7 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode
+
+ JvmProfiler.INSTANCE.onPacketSent(this.protocolInfo.id(), packetType, channelHandlerContext.channel().remoteAddress(), i);
+ } catch (Throwable var9) {
+- LOGGER.error("Error sending packet {}", packetType, var9);
++ LOGGER.error("Error sending packet {} (skippable? {})", packetType, packet.isSkippable(), var9);
+ if (packet.isSkippable()) {
+ throw new SkipPacketException(var9);
+ }
+diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
+index e113cd9d93750cf59712b06db62591876b4efbac..1789c0c71d968b386060bd6dc2630e8a078c32e2 100644
+--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
+@@ -47,4 +47,14 @@ public record ClientboundCustomQueryPacket(int transactionId, CustomQueryPayload
+ public void handle(ClientLoginPacketListener listener) {
+ listener.handleCustomQuery(this);
+ }
++
++ // Paper start - MC Utils - default query payloads
++ public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload {
++
++ @Override
++ public void write(final FriendlyByteBuf buf) {
++ buf.writeBytes(this.buffer.copy());
++ }
++ }
++ // Paper end - MC Utils - default query payloads
+ }
+diff --git a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
+index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b22404a031 100644
+--- a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
+@@ -20,7 +20,17 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
+ }
+
+ private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) {
+- return readUnknownPayload(buf);
++ // Paper start - MC Utils - default query payloads
++ FriendlyByteBuf buffer = buf.readNullable((buf2) -> {
++ int i = buf2.readableBytes();
++ if (i >= 0 && i <= MAX_PAYLOAD_SIZE) {
++ return new FriendlyByteBuf(buf2.readBytes(i));
++ } else {
++ throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes");
++ }
++ });
++ return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer);
++ // Paper end - MC Utils - default query payloads
+ }
+
+ private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) {
+@@ -47,4 +57,21 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
+ public void handle(ServerLoginPacketListener listener) {
+ listener.handleCustomQueryPacket(this);
+ }
++
++ // Paper start - MC Utils - default query payloads
++ public static final class QueryAnswerPayload implements CustomQueryAnswerPayload {
++
++ public final FriendlyByteBuf buffer;
++
++ public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) {
++ this.buffer = buffer;
++ }
++
++ @Override
++ public void write(final net.minecraft.network.FriendlyByteBuf buf) {
++ buf.writeBytes(this.buffer.copy());
++ }
++ }
++ // Paper end - MC Utils - default query payloads
++
+ }
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 294dc6691683b769b57635ea05b4b9e4562fa9f5..cbdc5f9c54f24ae09881b3c8dfe980f79d32ee4b 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -988,6 +988,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1);
+ }
+ // Spigot start
++ io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.shutdown(); // Paper
++ try { io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
++ } catch (java.lang.InterruptedException ignored) {} // Paper
+ if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
+ MinecraftServer.LOGGER.info("Saving usercache.json");
+ this.getProfileCache().save();
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 59bc334ade71c106e01e54db8d21fb65563dd3f1..b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -37,9 +37,9 @@ public class ChunkHolder extends GenerationChunkHolder {
+ public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
+ private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ private final LevelHeightAccessor levelHeightAccessor;
+- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture;
+- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture;
+- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture;
++ private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
++ private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
++ private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
+ public int oldTicketLevel;
+ private int ticketLevel;
+ private int queueLevel;
+@@ -101,7 +101,7 @@ public class ChunkHolder extends GenerationChunkHolder {
+ }
+
+ @Nullable
+- public LevelChunk getTickingChunk() {
++ public final LevelChunk getTickingChunk() { // Paper - final for inline
+ return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
+ }
+
+@@ -361,12 +361,28 @@ public class ChunkHolder extends GenerationChunkHolder {
+
+ this.wasAccessibleSinceLastSave |= flag1;
+ if (!flag && flag1) {
++ int expectCreateCount = ++this.fullChunkCreateCount; // Paper
+ this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
+ this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
++ // Paper start - cache ticking ready status
++ this.fullChunkFuture.thenAccept(chunkResult -> {
++ chunkResult.ifSuccess(chunk -> {
++ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
++ ChunkHolder.this.isFullChunkReady = true;
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
++ }
++ });
++ });
++ // Paper end - cache ticking ready status
+ this.addSaveDependency(this.fullChunkFuture);
+ }
+
+ if (flag && !flag1) {
++ // Paper start
++ if (this.isFullChunkReady) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ }
++ // Paper end
+ this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+ }
+@@ -377,11 +393,25 @@ public class ChunkHolder extends GenerationChunkHolder {
+ if (!flag2 && flag3) {
+ this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
+ this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
++ // Paper start - cache ticking ready status
++ this.tickingChunkFuture.thenAccept(chunkResult -> {
++ chunkResult.ifSuccess(chunk -> {
++ // note: Here is a very good place to add callbacks to logic waiting on this.
++ ChunkHolder.this.isTickingReady = true;
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
++ });
++ });
++ // Paper end
+ this.addSaveDependency(this.tickingChunkFuture);
+ }
+
+ if (flag2 && !flag3) {
+- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
++ // Paper start
++ if (this.isTickingReady) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ }
++ // Paper end
++ this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
+ this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+ }
+
+@@ -395,11 +425,24 @@ public class ChunkHolder extends GenerationChunkHolder {
+
+ this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
+ this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
++ // Paper start - cache ticking ready status
++ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
++ chunkResult.ifSuccess(chunk -> {
++ ChunkHolder.this.isEntityTickingReady = true;
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
++ });
++ });
++ // Paper end
+ this.addSaveDependency(this.entityTickingChunkFuture);
+ }
+
+ if (flag4 && !flag5) {
+- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
++ // Paper start
++ if (this.isEntityTickingReady) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
++ }
++ // Paper end
++ this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
+ this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+ }
+
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index ad2d29b106df33965090be521d4f945601965a00..261943f1f188643793a72bd239dfc5fe604e3b99 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -174,6 +174,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ };
+ // CraftBukkit end
+
++ // Paper start
++ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
++ return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++ // Paper end
++
+ public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
+ super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
+ this.visibleChunkMap = this.updatingChunkMap.clone();
+@@ -229,6 +235,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.chunksToEagerlySave.add(pos.toLong());
+ }
+
++ // Paper start
++ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
++ return -1;
++ }
++ // Paper end
++
+ protected ChunkGenerator generator() {
+ return this.worldGenContext.generator();
+ }
+@@ -385,9 +397,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ };
+
+ stringbuilder.append("Updating:").append(System.lineSeparator());
+- this.updatingChunkMap.values().forEach(consumer);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
+ stringbuilder.append("Visible:").append(System.lineSeparator());
+- this.visibleChunkMap.values().forEach(consumer);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
+ CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
+ CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
+
+@@ -429,6 +441,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ holder.setTicketLevel(level);
+ } else {
+ holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this);
++ // Paper start
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
++ // Paper end
+ }
+
+ this.updatingChunkMap.put(pos, holder);
+@@ -458,7 +473,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ protected void saveAllChunks(boolean flush) {
+ if (flush) {
+- List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
++ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
+ MutableBoolean mutableboolean = new MutableBoolean();
+
+ do {
+@@ -484,7 +499,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ } else {
+ this.nextChunkSaveTime.clear();
+ long i = Util.getMillis();
+- ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
++ Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+
+ while (objectiterator.hasNext()) {
+ ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
+@@ -509,7 +524,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ public boolean hasWork() {
+- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
++ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
+ }
+
+ private void processUnloads(BooleanSupplier shouldKeepTicking) {
+@@ -568,8 +583,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.scheduleUnload(pos, chunk);
+ } else {
+ ChunkAccess ichunkaccess = chunk.getLatestChunk();
+-
+- if (this.pendingUnloads.remove(pos, chunk) && ichunkaccess != null) {
++ // Paper start
++ boolean removed;
++ if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
++ // Paper end
+ LevelChunk chunk1;
+
+ if (ichunkaccess instanceof LevelChunk) {
+@@ -587,7 +605,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.lightEngine.tryScheduleUpdate();
+ this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
+ this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong());
+- }
++ } else if (removed) { // Paper start
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
++ } // Paper end
+
+ }
+ };
+@@ -936,7 +956,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ }
+
+- protected void setServerViewDistance(int watchDistance) {
++ public void setServerViewDistance(int watchDistance) { // Paper - public
+ int j = Mth.clamp(watchDistance, 2, 32);
+
+ if (j != this.serverViewDistance) {
+@@ -953,7 +973,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ }
+
+- int getPlayerViewDistance(ServerPlayer player) {
++ public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
+ return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
+ }
+
+@@ -982,7 +1002,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ public int size() {
+- return this.visibleChunkMap.size();
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
+ }
+
+ public DistanceManager getDistanceManager() {
+@@ -990,19 +1010,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ protected Iterable<ChunkHolder> getChunks() {
+- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
++ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
+ }
+
+ void dumpChunks(Writer writer) throws IOException {
+ CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
+ TickingTracker tickingtracker = this.distanceManager.tickingTracker();
+- ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
++ Iterator<ChunkHolder> objectbidirectionaliterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+
+ while (objectbidirectionaliterator.hasNext()) {
+- Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
+- long i = entry.getLongKey();
++ ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
++ long i = playerchunk.pos.toLong(); // Paper
+ ChunkPos chunkcoordintpair = new ChunkPos(i);
+- ChunkHolder playerchunk = (ChunkHolder) entry.getValue();
++ // Paper - move up
+ Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
+ Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
+ return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
+@@ -1445,7 +1465,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ });
+ }
+
+- private class ChunkDistanceManager extends DistanceManager {
++ public class ChunkDistanceManager extends DistanceManager { // Paper - public
+
+ protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
+ super(workerExecutor, mainThreadExecutor);
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index 83f9360d7c645c7bfca34627e40c86e2aa31143b..e7bca3db8c7d29fe984decddda83569ef921cc31 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -385,7 +385,7 @@ public abstract class DistanceManager {
+ }
+
+ public void removeTicketsOnClosing() {
+- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN);
++ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
+ ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
+
+ while (objectiterator.hasNext()) {
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 9cdcab885a915990a679f3fc9ae6885f7d125bfd..3e35a64b4b92ec25789e85c7445375dd899e1805 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -74,6 +74,13 @@ public class ServerChunkCache extends ChunkSource {
+ @Nullable
+ @VisibleForDebug
+ private NaturalSpawner.SpawnState lastSpawnState;
++ // Paper start
++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
++ public int getFullChunksCount() {
++ return this.fullChunks.size();
++ }
++ long chunkFutureAwaitCounter;
++ // Paper end
+
+ public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
+ this.level = world;
+@@ -104,6 +111,54 @@ public class ServerChunkCache extends ChunkSource {
+ return chunk.getFullChunkNow() != null;
+ }
+ // CraftBukkit end
++ // Paper start
++ public void addLoadedChunk(LevelChunk chunk) {
++ this.fullChunks.put(chunk.coordinateKey, chunk);
++ }
++
++ public void removeLoadedChunk(LevelChunk chunk) {
++ this.fullChunks.remove(chunk.coordinateKey);
++ }
++
++ @Nullable
++ public ChunkAccess getChunkAtImmediately(int x, int z) {
++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
++ if (holder == null) {
++ return null;
++ }
++
++ return holder.getLatestChunk();
++ }
++
++ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
++ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
++ }
++
++ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
++ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
++ }
++
++ // "real" get chunk if loaded
++ // Note: Partially copied from the getChunkAt method below
++ @Nullable
++ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) {
++ long k = ChunkPos.asLong(x, z);
++
++ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
++
++ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k);
++ if (playerChunk == null) {
++ return null;
++ }
++
++ return playerChunk.getFullChunkNowUnchecked();
++ }
++
++ @Nullable
++ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
++ return this.fullChunks.get(ChunkPos.asLong(x, z));
++ }
++ // Paper end
+
+ @Override
+ public ThreadedLevelLightEngine getLightEngine() {
+@@ -299,7 +354,7 @@ public class ServerChunkCache extends ChunkSource {
+ return this.mainThreadProcessor.pollTask();
+ }
+
+- boolean runDistanceManagerUpdates() {
++ public boolean runDistanceManagerUpdates() { // Paper - public
+ boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
+ boolean flag1 = this.chunkMap.promoteChunkMap();
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index ecbef5d54aef8e3f3bc2e4c34d2da6e96b1267b8..cba44ea8375692ce9d2511fba1ac1dd1d2d0cb1e 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -240,6 +240,103 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return this.convertable.dimensionType;
+ }
+
++ // Paper start
++ public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
++ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
++ // ICollisionAccess methods for VoxelShapes)
++ // be more strict too, add a block (dumb plugins in move events?)
++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
++
++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
++
++ int minChunkX = minBlockX >> 4;
++ int maxChunkX = maxBlockX >> 4;
++
++ int minChunkZ = minBlockZ >> 4;
++ int maxChunkZ = maxBlockZ >> 4;
++
++ ServerChunkCache chunkProvider = this.getChunkSource();
++
++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
++ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
++ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ if (Thread.currentThread() != this.thread) {
++ this.getChunkSource().mainThreadProcessor.execute(() -> {
++ this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
++ });
++ return;
++ }
++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
++
++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
++
++ int minChunkX = minBlockX >> 4;
++ int minChunkZ = minBlockZ >> 4;
++
++ int maxChunkX = maxBlockX >> 4;
++ int maxChunkZ = maxBlockZ >> 4;
++
++ this.loadChunks(minChunkX, minChunkZ, maxChunkX, maxChunkZ, priority, onLoad);
++ }
++
++ public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
++ ca.spottedleaf.concurrentutil.util.Priority priority,
++ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
++ it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
++ ServerChunkCache chunkProvider = this.getChunkSource();
++
++ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
++ int[] loadedChunks = new int[1];
++
++ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
++
++ java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
++ if (chunk != null) {
++ int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
++ ret.add(chunk);
++ ticketLevels.add(ticketLevel);
++ chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
++ }
++ if (++loadedChunks[0] == requiredChunks) {
++ try {
++ onLoad.accept(java.util.Collections.unmodifiableList(ret));
++ } finally {
++ for (int i = 0, len = ret.size(); i < len; ++i) {
++ ChunkPos chunkPos = ret.get(i).getPos();
++ int ticketLevel = ticketLevels.getInt(i);
++
++ chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
++ chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
++ }
++ }
++ }
++ };
++
++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
++ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
++ );
++ }
++ }
++ }
++ // Paper end
++
+ // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
+ public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
+ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 97a670499f4ce50239d8c09cf10c03a7520c8753..50c255c5226d50f78ead4c0a0694ac9d2df490f3 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -307,6 +307,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ public boolean sentListPacket = false;
+ public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
+ // CraftBukkit end
++ public boolean isRealPlayer; // Paper
+
+ public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
+ super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
+diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
+index 1ab1d4054c3fc343325d60f55b6af9bdac12305a..0fdb65a1e474950a50480f4e6f99c69f8e015a50 100644
+--- a/src/main/java/net/minecraft/server/level/TicketType.java
++++ b/src/main/java/net/minecraft/server/level/TicketType.java
+@@ -7,6 +7,7 @@ import net.minecraft.util.Unit;
+ import net.minecraft.world.level.ChunkPos;
+
+ public class TicketType<T> {
++ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
+
+ private final String name;
+ private final Comparator<T> comparator;
+diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+index a1f4ebcd0877a6d0c41493eff5d70a408bf98e59..f1725ef766c35aa623ace58fe8bf31fc9b2bb6b3 100644
+--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+@@ -169,6 +169,26 @@ public class WorldGenRegion implements WorldGenLevel {
+ return k < this.generatingStep.directDependencies().size();
+ }
+
++ // Paper start - if loaded util
++ @Nullable
++ @Override
++ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
++ return this.getChunk(x, z, ChunkStatus.FULL, false);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getBlockState(blockposition);
++ }
++
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getFluidState(blockposition);
++ }
++ // Paper end
++
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
+ return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos);
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index d7095aab94a9f3b9f06a033ba4d414dfae42f620..7bb87d2bdf0ead0fdca38a9685e2e15b249ec2cb 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -181,6 +181,7 @@ public abstract class PlayerList {
+ }
+
+ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
++ player.isRealPlayer = true; // Paper
+ GameProfile gameprofile = player.getGameProfile();
+ GameProfileCache usercache = this.server.getProfileCache();
+ // Optional optional; // CraftBukkit - decompile error
+diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
+index ae25aec117a7272735c824a00c1ed117fa52a921..d6e942aca1bcc769c390504a4119d6619872c4d4 100644
+--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
++++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
+@@ -82,6 +82,13 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
+ runnable.run();
+ }
+ }
++ // Paper start
++ public void scheduleOnMain(Runnable runnable) {
++ // postToMainThread does not work the same as older versions of mc
++ // This method is actually used to create a TickTask, which can then be posted onto main
++ this.schedule(this.wrapRunnable(runnable));
++ }
++ // Paper end
+
+ @Override
+ public void schedule(R runnable) {
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index 20cd4789d59de3255c04239a4ea49e9ebc5af1c0..cbbed9eff4c5fa5bcb67efd73cb15c539aa286b9 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -343,6 +343,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4);
+ }
+ // CraftBukkit end
++ // Paper start
++ public final AABB getBoundingBoxAt(double x, double y, double z) {
++ return this.dimensions.makeBoundingBox(x, y, z);
++ }
++ // Paper end
+
+ public Entity(EntityType<?> type, Level world) {
+ this.id = Entity.ENTITY_COUNTER.incrementAndGet();
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index 97960022b80fe19aec4df166b8e26c2f6100a583..d679363fb85e08c57e2886a24b88ee0a82afcf34 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -294,6 +294,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ public boolean collides = true;
+ public Set<UUID> collidableExemptions = new HashSet<>();
+ public boolean bukkitPickUpLoot;
++ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
+
+ @Override
+ public float getBukkitYaw() {
+diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
+index 75d14a20339dc170f478e3d1c1090ddfbc6adb5f..5fa11da0e1bfb0f0030746d1f58e40f09c04a221 100644
+--- a/src/main/java/net/minecraft/world/item/ItemStack.java
++++ b/src/main/java/net/minecraft/world/item/ItemStack.java
+@@ -1030,6 +1030,25 @@ public final class ItemStack implements DataComponentHolder {
+ }
+ }
+
++ // Paper start - (this is just a good no conflict location)
++ public org.bukkit.inventory.ItemStack asBukkitMirror() {
++ return CraftItemStack.asCraftMirror(this);
++ }
++ public org.bukkit.inventory.ItemStack asBukkitCopy() {
++ return CraftItemStack.asCraftMirror(this.copy());
++ }
++ public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
++ return CraftItemStack.asNMSCopy(itemstack);
++ }
++ private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
++ public org.bukkit.inventory.ItemStack getBukkitStack() {
++ if (bukkitStack == null || bukkitStack.handle != this) {
++ bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
++ }
++ return bukkitStack;
++ }
++ // Paper end
++
+ public void applyComponents(DataComponentPatch changes) {
+ this.components.applyPatch(changes);
+ this.getItem().verifyComponentsAfterLoad(this);
+@@ -1318,6 +1337,7 @@ public final class ItemStack implements DataComponentHolder {
+ // CraftBukkit start
+ @Deprecated
+ public void setItem(Item item) {
++ this.bukkitStack = null; // Paper
+ this.item = item;
+ }
+ // CraftBukkit end
+diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java
+index 083071d0f5e391d6e3bbda4aa84d8bb73e4f902b..f399d130d9bf4ab00f35090019a00c6287146490 100644
+--- a/src/main/java/net/minecraft/world/level/BlockGetter.java
++++ b/src/main/java/net/minecraft/world/level/BlockGetter.java
+@@ -12,6 +12,7 @@ import javax.annotation.Nullable;
+ import net.minecraft.core.BlockPos;
+ import net.minecraft.core.Direction;
+ import net.minecraft.util.Mth;
++import net.minecraft.world.level.block.Block;
+ import net.minecraft.world.level.block.entity.BlockEntity;
+ import net.minecraft.world.level.block.entity.BlockEntityType;
+ import net.minecraft.world.level.block.state.BlockState;
+@@ -35,6 +36,15 @@ public interface BlockGetter extends LevelHeightAccessor {
+ }
+
+ BlockState getBlockState(BlockPos pos);
++ // Paper start - if loaded util
++ @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition);
++
++ default @Nullable Block getBlockIfLoaded(BlockPos blockposition) {
++ BlockState type = this.getBlockStateIfLoaded(blockposition);
++ return type == null ? null : type.getBlock();
++ }
++ @Nullable FluidState getFluidIfLoaded(BlockPos blockposition);
++ // Paper end
+
+ FluidState getFluidState(BlockPos pos);
+
+diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java
+index a59d3f56859ba7c07dcb45b1e199e3a7132dcff7..0639e4565c3324d757dec1226adb4e99d841f2c0 100644
+--- a/src/main/java/net/minecraft/world/level/ChunkPos.java
++++ b/src/main/java/net/minecraft/world/level/ChunkPos.java
+@@ -46,6 +46,7 @@ public class ChunkPos {
+ public static final int REGION_MAX_INDEX = 31;
+ public final int x;
+ public final int z;
++ public final long longKey; // Paper
+ private static final int HASH_A = 1664525;
+ private static final int HASH_C = 1013904223;
+ private static final int HASH_Z_XOR = -559038737;
+@@ -53,16 +54,19 @@ public class ChunkPos {
+ public ChunkPos(int x, int z) {
+ this.x = x;
+ this.z = z;
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public ChunkPos(BlockPos pos) {
+ this.x = SectionPos.blockToSectionCoord(pos.getX());
+ this.z = SectionPos.blockToSectionCoord(pos.getZ());
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public ChunkPos(long pos) {
+ this.x = (int)pos;
+ this.z = (int)(pos >> 32);
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public static ChunkPos minFromRegion(int x, int z) {
+@@ -74,7 +78,7 @@ public class ChunkPos {
+ }
+
+ public long toLong() {
+- return asLong(this.x, this.z);
++ return longKey; // Paper
+ }
+
+ public static long asLong(int chunkX, int chunkZ) {
+diff --git a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
+index 8741264b0a9529b40c41a4b9bf25b50d27c830bc..87af0b3cfd3a3170955894028fd667f108ea8121 100644
+--- a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
++++ b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
+@@ -17,6 +17,18 @@ public enum EmptyBlockGetter implements BlockGetter {
+ return null;
+ }
+
++ // Paper start - If loaded util
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ return Fluids.EMPTY.defaultFluidState();
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ return Blocks.AIR.defaultBlockState();
++ }
++ // Paper end
++
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
+ return Blocks.AIR.defaultBlockState();
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 396d7425ee3754e502b23924001b00c57c24016e..a3fd246b6a09a77fa64ef8e435edadf77dfbb1d7 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -98,6 +98,7 @@ import org.bukkit.craftbukkit.CraftServer;
+ import org.bukkit.craftbukkit.CraftWorld;
+ import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+ import org.bukkit.craftbukkit.block.CapturedBlockState;
++import org.bukkit.craftbukkit.block.CraftBlockState;
+ import org.bukkit.craftbukkit.block.data.CraftBlockData;
+ import org.bukkit.craftbukkit.util.CraftSpawnCategory;
+ import org.bukkit.entity.SpawnCategory;
+@@ -275,6 +276,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ return null;
+ }
+
++ // Paper start
++ public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) {
++ // To be patched over
++ return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType();
++ }
++ // Paper end
++
+ public boolean isInWorldBounds(BlockPos pos) {
+ return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
+ }
+@@ -291,18 +299,52 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ return y < -20000000 || y >= 20000000;
+ }
+
+- public LevelChunk getChunkAt(BlockPos pos) {
++ public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline
+ return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+ }
+
+ @Override
+- public LevelChunk getChunk(int chunkX, int chunkZ) {
+- return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
++ public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
++ return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
+ }
+
++ // Paper start - if loaded
++ @Nullable
++ @Override
++ public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
++ return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
++ }
++
++ @Override
+ @Nullable
++ public final BlockState getBlockStateIfLoaded(BlockPos pos) {
++ // CraftBukkit start - tree generation
++ if (this.captureTreeGeneration) {
++ CraftBlockState previous = this.capturedBlockStates.get(pos);
++ if (previous != null) {
++ return previous.getHandle();
++ }
++ }
++ // CraftBukkit end
++ if (this.isOutsideBuildHeight(pos)) {
++ return Blocks.VOID_AIR.defaultBlockState();
++ } else {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
++
++ return chunk == null ? null : chunk.getBlockState(pos);
++ }
++ }
++
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++
++ return chunk == null ? null : chunk.getFluidState(blockposition);
++ }
++
+ @Override
+ public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
++ // Paper end
+ ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
+
+ if (ichunkaccess == null && create) {
+@@ -554,7 +596,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ if (this.isOutsideBuildHeight(pos)) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ } else {
+- LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
++ ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
+
+ return chunk.getBlockState(pos);
+ }
+diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
+index 35b338fed1df844846ecc876cbbab19e0efbfcb0..5eb8982678110fabb82a93c5ec67c666b7fde017 100644
+--- a/src/main/java/net/minecraft/world/level/LevelReader.java
++++ b/src/main/java/net/minecraft/world/level/LevelReader.java
+@@ -26,6 +26,9 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal
+ @Nullable
+ ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+
++ @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
++ @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
++
+ @Deprecated
+ boolean hasChunk(int chunkX, int chunkZ);
+
+diff --git a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
+index b682c7c51204f727e21bfc20c691837deb7137ba..5375ea8dee8f74c843964824ad1a374e15711b60 100644
+--- a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
++++ b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
+@@ -8,6 +8,7 @@ import net.minecraft.core.BlockPos;
+ import net.minecraft.core.Holder;
+ import net.minecraft.core.SectionPos;
+ import net.minecraft.core.registries.Registries;
++import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.level.biome.Biome;
+ import net.minecraft.world.level.biome.Biomes;
+@@ -66,7 +67,7 @@ public class PathNavigationRegion implements CollisionGetter {
+ private ChunkAccess getChunk(int chunkX, int chunkZ) {
+ int i = chunkX - this.centerX;
+ int j = chunkZ - this.centerZ;
+- if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) {
++ if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
+ ChunkAccess chunkAccess = this.chunks[i][j];
+ return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get()));
+ } else {
+@@ -74,6 +75,30 @@ public class PathNavigationRegion implements CollisionGetter {
+ }
+ }
+
++ // Paper start - if loaded util
++ private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) {
++ // Based on getChunk(int, int)
++ int xx = x - this.centerX;
++ int zz = z - this.centerZ;
++
++ if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) {
++ return this.chunks[xx][zz];
++ }
++ return null;
++ }
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getFluidState(blockposition);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getBlockState(blockposition);
++ }
++ // Paper end
++
+ @Override
+ public WorldBorder getWorldBorder() {
+ return this.level.getWorldBorder();
+diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+index 5322157822b724bc4808277d2e69020c20256d9d..4315fc2473bd60a14ce848e4c43001f04eb0cd9f 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+@@ -887,12 +887,14 @@ public abstract class BlockBehaviour implements FeatureElement {
+ }
+ }
+
++ protected boolean shapeExceedsCube = true; // Paper - moved from actual method to here
+ public void initCache() {
+ this.fluidState = ((Block) this.owner).getFluidState(this.asState());
+ this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState());
+ if (!this.getBlock().hasDynamicShape()) {
+ this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
+ }
++ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
+
+ this.legacySolid = this.calculateSolid();
+ this.occlusionShape = this.canOcclude ? ((Block) this.owner).getOcclusionShape(this.asState()) : Shapes.empty();
+@@ -959,8 +961,8 @@ public abstract class BlockBehaviour implements FeatureElement {
+ return this.occlusionShape;
+ }
+
+- public boolean hasLargeCollisionShape() {
+- return this.cache == null || this.cache.largeCollisionShape;
++ public final boolean hasLargeCollisionShape() { // Paper
++ return this.shapeExceedsCube; // Paper - moved into shape cache init
+ }
+
+ public boolean useShapeForLightOcclusion() {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+index daf0ff02807ca658fffb62e9914f7b2c6e4ae5ea..4b56e7cb45026ac3323ecf0b9622a177e961112d 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -65,7 +65,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+ protected final ShortList[] postProcessing;
+ private volatile boolean unsaved;
+ private volatile boolean isLightCorrect;
+- protected final ChunkPos chunkPos;
++ protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key
+ private long inhabitedTime;
+ /** @deprecated */
+ @Nullable
+@@ -91,7 +91,8 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+ // CraftBukkit end
+
+ public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
+- this.chunkPos = pos;
++ this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
++ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
+ this.upgradeData = upgradeData;
+ this.levelHeightAccessor = heightLimitView;
+ this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()];
+diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793603852f9 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+@@ -25,6 +25,12 @@ public class EmptyLevelChunk extends LevelChunk {
+ public BlockState getBlockState(BlockPos pos) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ }
++ // Paper start
++ @Override
++ public BlockState getBlockState(final int x, final int y, final int z) {
++ return Blocks.VOID_AIR.defaultBlockState();
++ }
++ // Paper end
+
+ @Nullable
+ @Override
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index b182b4fdfde60a67d096bb65d9dad5f893c8bb4b..9e6889d20bc2c9e86103f6d935d344de3ec48050 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -121,6 +121,10 @@ public class LevelChunk extends ChunkAccess {
+ public boolean needsDecoration;
+ // CraftBukkit end
+
++ // Paper start
++ boolean loadedTicketLevel;
++ // Paper end
++
+ public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
+ this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
+ if (!Collections.disjoint(protoChunk.pendingBlockEntities.keySet(), protoChunk.blockEntities.keySet())) {
+@@ -209,8 +213,25 @@ public class LevelChunk extends ChunkAccess {
+ }
+ }
+
++ // Paper start - Perf: Reduce instructions and provide final method
++ public BlockState getBlockState(final int x, final int y, final int z) {
++ return this.getBlockStateFinal(x, y, z);
++ }
++ public BlockState getBlockStateFinal(final int x, final int y, final int z) {
++ // Copied and modified from below
++ final int sectionIndex = this.getSectionIndex(y);
++ if (sectionIndex < 0 || sectionIndex >= this.sections.length
++ || this.sections[sectionIndex].nonEmptyBlockCount == 0) {
++ return Blocks.AIR.defaultBlockState();
++ }
++ return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
++ }
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
++ if (true) {
++ return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
++ }
++ // Paper end - Perf: Reduce instructions and provide final method
+ int i = pos.getX();
+ int j = pos.getY();
+ int k = pos.getZ();
+@@ -252,6 +273,18 @@ public class LevelChunk extends ChunkAccess {
+ }
+ }
+
++ // Paper start - If loaded util
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ return this.getFluidState(blockposition);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ return this.getBlockState(blockposition);
++ }
++ // Paper end
++
+ @Override
+ public FluidState getFluidState(BlockPos pos) {
+ return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
+@@ -584,7 +617,11 @@ public class LevelChunk extends ChunkAccess {
+
+ // CraftBukkit start
+ public void loadCallback() {
++ // Paper start
++ this.loadedTicketLevel = true;
++ // Paper end
+ org.bukkit.Server server = this.level.getCraftServer();
++ this.level.getChunkSource().addLoadedChunk(this); // Paper
+ if (server != null) {
+ /*
+ * If it's a new world, the first few chunks are generated inside
+@@ -625,6 +662,10 @@ public class LevelChunk extends ChunkAccess {
+ server.getPluginManager().callEvent(unloadEvent);
+ // note: saving can be prevented, but not forced if no saving is actually required
+ this.mustNotSave = !unloadEvent.isSaveChunk();
++ this.level.getChunkSource().removeLoadedChunk(this); // Paper
++ // Paper start
++ this.loadedTicketLevel = false;
++ // Paper end
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+index 0cf916776ce8735dcfa4c765b18e77037501a322..771529ba28a16664ad19ed9c0f4bf95eeb7da76b 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -19,7 +19,7 @@ public class LevelChunkSection {
+ public static final int SECTION_HEIGHT = 16;
+ public static final int SECTION_SIZE = 4096;
+ public static final int BIOME_CONTAINER_BITS = 2;
+- private short nonEmptyBlockCount;
++ short nonEmptyBlockCount; // Paper - package private
+ private short tickingBlockCount;
+ private short tickingFluidCount;
+ public final PalettedContainer<BlockState> states;
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+index 1d7d057b1ccc9074863ab51c1898114ece5e2d7a..fe8b9373a4906652bb6c0b3d8e1d83df04d592a2 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+@@ -83,6 +83,18 @@ public class ProtoChunk extends ChunkAccess {
+ return new ChunkAccess.PackedTicks(this.blockTicks.pack(time), this.fluidTicks.pack(time));
+ }
+
++ // Paper start - If loaded util
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ return this.getFluidState(blockposition);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ return this.getBlockState(blockposition);
++ }
++ // Paper end
++
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
+ int i = pos.getY();
+diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+index d2104e6e6ac7911bdba1cea3b9eca64930165cce..459a20453d3b447cae20fbe6426dfdc62a34949a 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+@@ -168,7 +168,7 @@ public class ChunkStatusTasks {
+ }, context.mainThreadExecutor());
+ }
+
+- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
++ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public
+ if (!entities.isEmpty()) {
+ // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
+ world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD).filter((entity) -> {
+diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+index 34933c5324126f9afdc5cba9dea997ace8f01806..4eb0b0969325f39a7ae65492cccd482515a50142 100644
+--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+@@ -91,6 +91,16 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
+ }
+
+ private boolean addEntity(T entity, boolean existing) {
++ // Paper start - chunk system hooks
++ // I don't want to know why this is a generic type.
++ Entity entityCasted = (Entity)entity;
++ boolean wasRemoved = entityCasted.isRemoved();
++ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, existing, true);
++ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
++ // removed by callback
++ return false;
++ }
++ // Paper end - chunk system hooks
+ if (!this.addEntityUuid(entity)) {
+ return false;
+ } else {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index d5451cb1976ca3675dd19b07bd8a2d363f82db86..e5054699f2f7555455b4da20249e253dba7043b4 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -2627,4 +2627,9 @@ public final class CraftServer implements Server {
+ return this.spigot;
+ }
+ // Spigot end
++
++ @Override
++ public double[] getTPS() {
++ return new double[]{0, 0, 0}; // TODO
++ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index a9dec31bdafd6bae677e58143fe618d812b338b7..8e5a6137321d1d4941de8be2af5c7a3e5e143cf1 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -257,8 +257,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+
+ @Override
+ public Chunk[] getLoadedChunks() {
+- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
+- return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
++ List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
++ return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
+ }
+
+ @Override
+@@ -335,7 +335,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+
+ @Override
+ public boolean refreshChunk(int x, int z) {
+- ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z));
++ ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
+ if (playerChunk == null) return false;
+
+ playerChunk.getTickingChunkFuture().thenAccept(either -> {
+@@ -2115,4 +2115,51 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ return this.spigot;
+ }
+ // Spigot end
++ // Paper start
++ @Override
++ public void getChunkAtAsync(int x, int z, boolean gen, boolean urgent, @NotNull Consumer<? super Chunk> cb) {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
++ this.getHandle(), x, z, gen, ChunkStatus.FULL, true,
++ urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
++ (ChunkAccess chunk) -> {
++ cb.accept(chunk == null ? null : new CraftChunk((net.minecraft.world.level.chunk.LevelChunk)chunk));
++ }
++ );
++
++ }
++
++ @Override
++ public void getChunksAtAsync(int minX, int minZ, int maxX, int maxZ, boolean urgent, Runnable cb) {
++ this.getHandle().loadChunks(
++ minX, minZ, maxX, maxZ,
++ urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
++ (List<ChunkAccess> chunks) -> {
++ cb.run();
++ }
++ );
++ }
++
++ @Override
++ public void setViewDistance(final int viewDistance) {
++ if (viewDistance < 2 || viewDistance > 32) {
++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
++ }
++ this.getHandle().chunkSource.chunkMap.setServerViewDistance(viewDistance);
++ }
++
++ @Override
++ public void setSimulationDistance(final int simulationDistance) {
++ throw new UnsupportedOperationException("Not implemented yet");
++ }
++
++ @Override
++ public int getSendViewDistance() {
++ return this.getViewDistance();
++ }
++
++ @Override
++ public void setSendViewDistance(final int viewDistance) {
++ throw new UnsupportedOperationException("Not implemented yet");
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 9f6e15c138e708856cd69ebd4e1a26a2ee259206..e8ff50f1d799984b49116ef2dd1be70e3a655a10 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2432,4 +2432,34 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ return this.spigot;
+ }
+ // Spigot end
++
++ @Override
++ public int getViewDistance() {
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getViewDistance(this.getHandle());
++ }
++
++ @Override
++ public void setViewDistance(final int viewDistance) {
++ throw new UnsupportedOperationException("Not implemented yet");
++ }
++
++ @Override
++ public int getSimulationDistance() {
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
++ }
++
++ @Override
++ public void setSimulationDistance(final int simulationDistance) {
++ throw new UnsupportedOperationException("Not implemented yet");
++ }
++
++ @Override
++ public int getSendViewDistance() {
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
++ }
++
++ @Override
++ public void setSendViewDistance(final int viewDistance) {
++ throw new UnsupportedOperationException("Not implemented yet");
++ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+index f6e6f0ddef6693c58f28b89cf3df005a8d47e08d..101eea3452c9e387e770b716543c3a4f17b9a737 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -31,6 +31,20 @@ import org.jetbrains.annotations.ApiStatus;
+ @DelegateDeserialization(ItemStack.class)
+ public final class CraftItemStack extends ItemStack {
+
++ // Paper start - MC Utils
++ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) {
++ if (bukkit instanceof CraftItemStack craftItemStack) {
++ return craftItemStack.handle != null ? craftItemStack.handle : net.minecraft.world.item.ItemStack.EMPTY;
++ } else {
++ return asNMSCopy(bukkit);
++ }
++ }
++
++ public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) {
++ return old == newInstance ? unwrap(old) : asNMSCopy(newInstance);
++ }
++ // Paper end - MC Utils
++
+ public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) {
+ if (original instanceof CraftItemStack) {
+ CraftItemStack stack = (CraftItemStack) original;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
+index 097996d3955ab5126b71f7bff1dd2c62becb5ffd..a8b46ea5e4b6260c2728c67e8651b74fe6356605 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
+@@ -40,6 +40,17 @@ public final class CraftLocation {
+ return new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
+ }
+
++ // Paper start
++ public static net.minecraft.core.GlobalPos toGlobalPos(Location location) {
++ return net.minecraft.core.GlobalPos.of(((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle().dimension(), toBlockPosition(location));
++ }
++
++ public static Location fromGlobalPos(net.minecraft.core.GlobalPos globalPos) {
++ BlockPos pos = globalPos.pos();
++ return new org.bukkit.Location(net.minecraft.server.MinecraftServer.getServer().getLevel(globalPos.dimension()).getWorld(), pos.getX(), pos.getY(), pos.getZ());
++ }
++ // Paper end
++
+ public static Vec3 toVec3D(Location location) {
+ return new Vec3(location.getX(), location.getY(), location.getZ());
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+index b8a865305cc61954aeebff4a7cd1d1973c5f46ab..e444662ee4d9405eeea7caa41b9cd6b36586d840 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+@@ -56,6 +56,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
+ import net.minecraft.world.ticks.LevelTickAccess;
+ import net.minecraft.world.ticks.TickPriority;
+ import org.bukkit.event.entity.CreatureSpawnEvent;
++import org.jetbrains.annotations.Nullable;
+
+ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
+
+@@ -788,4 +789,25 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
+ public boolean isFluidAtPosition(BlockPos pos, Predicate<FluidState> state) {
+ return this.handle.isFluidAtPosition(pos, state);
+ }
++
++ // Paper start
++ @Nullable
++ @Override
++ public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
++ return this.handle.getBlockStateIfLoaded(blockposition);
++ }
++
++ @Nullable
++ @Override
++ public FluidState getFluidIfLoaded(final BlockPos blockposition) {
++ return this.handle.getFluidIfLoaded(blockposition);
++ }
++
++ @Nullable
++ @Override
++ public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
++ return this.handle.getChunkIfLoadedImmediately(x, z);
++ }
++ // Paper end
+ }
++
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
+index e837d76e833d73d888bc1dad3515c2b82bc0e437..4705aed1dd98378c146bf9e346df1a17f719ad36 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
+@@ -212,7 +212,23 @@ public class DummyGeneratorAccess implements WorldGenLevel {
+ public FluidState getFluidState(BlockPos pos) {
+ return Fluids.EMPTY.defaultFluidState(); // SPIGOT-6634
+ }
++ // Paper start - if loaded util
++ @javax.annotation.Nullable
++ @Override
++ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
++ throw new UnsupportedOperationException("Not supported yet.");
++ }
++
++ @Override
++ public BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ throw new UnsupportedOperationException("Not supported yet.");
++ }
+
++ @Override
++ public FluidState getFluidIfLoaded(BlockPos blockposition) {
++ throw new UnsupportedOperationException("Not supported yet.");
++ }
++ // Paper end
+ @Override
+ public WorldBorder getWorldBorder() {
+ throw new UnsupportedOperationException("Not supported yet.");
+diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
+index fb83cadf384712e2dff1cb001bdbeec8f5e89ada..5baf68732cb0e5ecab9d809df54a42e8252f1624 100644
+--- a/src/main/java/org/spigotmc/ActivationRange.java
++++ b/src/main/java/org/spigotmc/ActivationRange.java
+@@ -35,6 +35,9 @@ public class ActivationRange
+
+ public enum ActivationType
+ {
++ WATER, // Paper
++ FLYING_MONSTER, // Paper
++ VILLAGER, // Paper
+ MONSTER,
+ ANIMAL,
+ RAIDER,
+diff --git a/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
+new file mode 100644
+index 0000000000000000000000000000000000000000..e57c3ca79677b1dfe7cf3db36f0406de7ea5bd0a
+--- /dev/null
++++ b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
+@@ -0,0 +1 @@
++ca.spottedleaf.moonrise.paper.PaperHooks
diff --git a/patches/server/0010-Adventure.patch b/patches/server/0010-Adventure.patch
new file mode 100644
index 0000000000..8a0288ddb0
--- /dev/null
+++ b/patches/server/0010-Adventure.patch
@@ -0,0 +1,6280 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Riley Park <[email protected]>
+Date: Fri, 29 Jan 2021 17:54:03 +0100
+Subject: [PATCH] Adventure
+
+== AT ==
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo item
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo count
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo components
+public net.minecraft.network.chat.contents.TranslatableContents filterAllowedArguments(Ljava/lang/Object;)Lcom/mojang/serialization/DataResult;
+
+Co-authored-by: zml <[email protected]>
+Co-authored-by: Jake Potrebic <[email protected]>
+
+diff --git a/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2c5702a42c4a3d8b37deeb26e1bd7fbdcca3554e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/AdventureCodecs.java
+@@ -0,0 +1,450 @@
++package io.papermc.paper.adventure;
++
++import com.google.gson.JsonElement;
++import com.google.gson.JsonParser;
++import com.mojang.brigadier.exceptions.CommandSyntaxException;
++import com.mojang.datafixers.util.Either;
++import com.mojang.datafixers.util.Pair;
++import com.mojang.serialization.Codec;
++import com.mojang.serialization.DataResult;
++import com.mojang.serialization.DynamicOps;
++import com.mojang.serialization.JsonOps;
++import com.mojang.serialization.MapCodec;
++import com.mojang.serialization.codecs.RecordCodecBuilder;
++import java.io.IOException;
++import java.util.Collections;
++import java.util.List;
++import java.util.Map;
++import java.util.Optional;
++import java.util.UUID;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.Predicate;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.text.BlockNBTComponent;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.EntityNBTComponent;
++import net.kyori.adventure.text.KeybindComponent;
++import net.kyori.adventure.text.NBTComponent;
++import net.kyori.adventure.text.NBTComponentBuilder;
++import net.kyori.adventure.text.ScoreComponent;
++import net.kyori.adventure.text.SelectorComponent;
++import net.kyori.adventure.text.StorageNBTComponent;
++import net.kyori.adventure.text.TextComponent;
++import net.kyori.adventure.text.TranslatableComponent;
++import net.kyori.adventure.text.TranslationArgument;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.event.DataComponentValue;
++import net.kyori.adventure.text.event.HoverEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.Style;
++import net.kyori.adventure.text.format.TextColor;
++import net.kyori.adventure.text.format.TextDecoration;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import net.minecraft.commands.arguments.selector.SelectorPattern;
++import net.minecraft.core.UUIDUtil;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.nbt.TagParser;
++import net.minecraft.network.RegistryFriendlyByteBuf;
++import net.minecraft.network.chat.ComponentSerialization;
++import net.minecraft.network.chat.contents.KeybindContents;
++import net.minecraft.network.chat.contents.ScoreContents;
++import net.minecraft.network.chat.contents.TranslatableContents;
++import net.minecraft.network.codec.ByteBufCodecs;
++import net.minecraft.network.codec.StreamCodec;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.util.ExtraCodecs;
++import net.minecraft.util.StringRepresentable;
++import net.minecraft.world.item.Item;
++import net.minecraft.world.item.ItemStack;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.intellij.lang.annotations.Subst;
++
++import static com.mojang.serialization.Codec.recursive;
++import static com.mojang.serialization.codecs.RecordCodecBuilder.mapCodec;
++import static java.util.function.Function.identity;
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.TranslationArgument.bool;
++import static net.kyori.adventure.text.TranslationArgument.component;
++import static net.kyori.adventure.text.TranslationArgument.numeric;
++
++@DefaultQualifier(NonNull.class)
++public final class AdventureCodecs {
++
++ public static final Codec<Component> COMPONENT_CODEC = recursive("adventure Component", AdventureCodecs::createCodec);
++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_COMPONENT_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(COMPONENT_CODEC);
++
++ static final Codec<TextColor> TEXT_COLOR_CODEC = Codec.STRING.comapFlatMap(s -> {
++ if (s.startsWith("#")) {
++ @Nullable TextColor value = TextColor.fromHexString(s);
++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure TextColor");
++ } else {
++ final @Nullable NamedTextColor value = NamedTextColor.NAMES.value(s);
++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure NamedTextColor");
++ }
++ }, textColor -> {
++ if (textColor instanceof NamedTextColor named) {
++ return NamedTextColor.NAMES.keyOrThrow(named);
++ } else {
++ return textColor.asHexString();
++ }
++ });
++
++ static final Codec<Key> KEY_CODEC = Codec.STRING.comapFlatMap(s -> {
++ return Key.parseable(s) ? DataResult.success(Key.key(s)) : DataResult.error(() -> "Cannot convert " + s + " to adventure Key");
++ }, Key::asString);
++
++ static final Codec<ClickEvent.Action> CLICK_EVENT_ACTION_CODEC = Codec.STRING.comapFlatMap(s -> {
++ final ClickEvent.@Nullable Action value = ClickEvent.Action.NAMES.value(s);
++ return value != null ? DataResult.success(value) : DataResult.error(() -> "Cannot convert " + s + " to adventure ClickEvent$Action");
++ }, ClickEvent.Action.NAMES::keyOrThrow);
++ static final Codec<ClickEvent> CLICK_EVENT_CODEC = RecordCodecBuilder.create((instance) -> {
++ return instance.group(
++ CLICK_EVENT_ACTION_CODEC.fieldOf("action").forGetter(ClickEvent::action),
++ Codec.STRING.fieldOf("value").forGetter(ClickEvent::value)
++ ).apply(instance, ClickEvent::clickEvent);
++ });
++
++ static Codec<HoverEvent.ShowEntity> showEntityCodec(final Codec<Component> componentCodec) {
++ return RecordCodecBuilder.create((instance) -> {
++ return instance.group(
++ KEY_CODEC.fieldOf("type").forGetter(HoverEvent.ShowEntity::type),
++ UUIDUtil.LENIENT_CODEC.fieldOf("id").forGetter(HoverEvent.ShowEntity::id),
++ componentCodec.lenientOptionalFieldOf("name").forGetter(he -> Optional.ofNullable(he.name()))
++ ).apply(instance, (key, uuid, component) -> {
++ return HoverEvent.ShowEntity.showEntity(key, uuid, component.orElse(null));
++ });
++ });
++ }
++
++ static Codec<HoverEvent.ShowItem> showItemCodec(final Codec<Component> componentCodec) {
++ return net.minecraft.network.chat.HoverEvent.ItemStackInfo.CODEC.xmap(isi -> {
++ @Subst("key") final String typeKey = isi.item.unwrapKey().orElseThrow().location().toString();
++ return HoverEvent.ShowItem.showItem(Key.key(typeKey), isi.count, PaperAdventure.asAdventure(isi.getItemStack().getComponentsPatch()));
++ }, si -> {
++ final Item itemType = BuiltInRegistries.ITEM.getValue(PaperAdventure.asVanilla(si.item()));
++ final Map<Key, DataComponentValue> dataComponentsMap = si.dataComponents();
++ final ItemStack stack = new ItemStack(BuiltInRegistries.ITEM.wrapAsHolder(itemType), si.count(), PaperAdventure.asVanilla(dataComponentsMap));
++ return new net.minecraft.network.chat.HoverEvent.ItemStackInfo(stack);
++ });
++ }
++
++ static final HoverEventType<HoverEvent.ShowEntity> SHOW_ENTITY_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showEntityCodec, HoverEvent.Action.SHOW_ENTITY, "show_entity", AdventureCodecs::legacyDeserializeEntity);
++ static final HoverEventType<HoverEvent.ShowItem> SHOW_ITEM_HOVER_EVENT_TYPE = new HoverEventType<>(AdventureCodecs::showItemCodec, HoverEvent.Action.SHOW_ITEM, "show_item", AdventureCodecs::legacyDeserializeItem);
++ static final HoverEventType<Component> SHOW_TEXT_HOVER_EVENT_TYPE = new HoverEventType<>(identity(), HoverEvent.Action.SHOW_TEXT, "show_text", (component, registryOps, codec) -> DataResult.success(component));
++ static final Codec<HoverEventType<?>> HOVER_EVENT_TYPE_CODEC = StringRepresentable.fromValues(() -> new HoverEventType<?>[]{ SHOW_ENTITY_HOVER_EVENT_TYPE, SHOW_ITEM_HOVER_EVENT_TYPE, SHOW_TEXT_HOVER_EVENT_TYPE });
++
++ static DataResult<HoverEvent.ShowEntity> legacyDeserializeEntity(final Component component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) {
++ try {
++ final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component));
++ final DynamicOps<JsonElement> dynamicOps = ops != null ? ops.withParent(JsonOps.INSTANCE) : JsonOps.INSTANCE;
++ final DataResult<Component> entityNameResult = componentCodec.parse(dynamicOps, JsonParser.parseString(tag.getString("name")));
++ @Subst("key") final String keyString = tag.getString("type");
++ final UUID entityUUID = UUID.fromString(tag.getString("id"));
++ return entityNameResult.map(name -> HoverEvent.ShowEntity.showEntity(Key.key(keyString), entityUUID, name));
++ } catch (final Exception ex) {
++ return DataResult.error(() -> "Failed to parse tooltip: " + ex.getMessage());
++ }
++ }
++
++ static DataResult<HoverEvent.ShowItem> legacyDeserializeItem(final Component component, final @Nullable RegistryOps<?> ops, final Codec<Component> componentCodec) {
++ try {
++ final CompoundTag tag = TagParser.parseTag(PlainTextComponentSerializer.plainText().serialize(component));
++ final DynamicOps<Tag> dynamicOps = ops != null ? ops.withParent(NbtOps.INSTANCE) : NbtOps.INSTANCE;
++ final DataResult<ItemStack> stackResult = ItemStack.CODEC.parse(dynamicOps, tag);
++ return stackResult.map(stack -> {
++ @Subst("key:value") final String location = stack.getItemHolder().unwrapKey().orElseThrow().location().toString();
++ return HoverEvent.ShowItem.showItem(Key.key(location), stack.getCount(), PaperAdventure.asAdventure(stack.getComponentsPatch()));
++ });
++ } catch (final CommandSyntaxException ex) {
++ return DataResult.error(() -> "Failed to parse item tag: " + ex.getMessage());
++ }
++ }
++
++ @FunctionalInterface
++ interface LegacyDeserializer<T> {
++ DataResult<T> apply(Component component, @Nullable RegistryOps<?> ops, Codec<Component> componentCodec);
++ }
++
++ record HoverEventType<V>(Function<Codec<Component>, MapCodec<HoverEvent<V>>> codec, String id, Function<Codec<Component>, MapCodec<HoverEvent<V>>> legacyCodec) implements StringRepresentable {
++ HoverEventType(final Function<Codec<Component>, Codec<V>> contentCodec, final HoverEvent.Action<V> action, final String id, final LegacyDeserializer<V> legacyDeserializer) {
++ this(cc -> contentCodec.apply(cc).xmap(v -> HoverEvent.hoverEvent(action, v), HoverEvent::value).fieldOf("contents"),
++ id,
++ codec -> (new Codec<HoverEvent<V>>() {
++ public <D> DataResult<Pair<HoverEvent<V>, D>> decode(final DynamicOps<D> dynamicOps, final D object) {
++ return codec.decode(dynamicOps, object).flatMap(pair -> {
++ final DataResult<V> dataResult;
++ if (dynamicOps instanceof final RegistryOps<D> registryOps) {
++ dataResult = legacyDeserializer.apply(pair.getFirst(), registryOps, codec);
++ } else {
++ dataResult = legacyDeserializer.apply(pair.getFirst(), null, codec);
++ }
++
++ return dataResult.map(value -> Pair.of(HoverEvent.hoverEvent(action, value), pair.getSecond()));
++ });
++ }
++
++ public <D> DataResult<D> encode(final HoverEvent<V> hoverEvent, final DynamicOps<D> dynamicOps, final D object) {
++ return DataResult.error(() -> "Can't encode in legacy format");
++ }
++ }).fieldOf("value")
++ );
++ }
++ @Override
++ public String getSerializedName() {
++ return this.id;
++ }
++ }
++
++ private static final Function<HoverEvent<?>, HoverEventType<?>> GET_HOVER_EVENT_TYPE = he -> {
++ if (he.action() == HoverEvent.Action.SHOW_ENTITY) {
++ return SHOW_ENTITY_HOVER_EVENT_TYPE;
++ } else if (he.action() == HoverEvent.Action.SHOW_ITEM) {
++ return SHOW_ITEM_HOVER_EVENT_TYPE;
++ } else if (he.action() == HoverEvent.Action.SHOW_TEXT) {
++ return SHOW_TEXT_HOVER_EVENT_TYPE;
++ } else {
++ throw new IllegalStateException();
++ }
++ };
++ static final Codec<HoverEvent<?>> HOVER_EVENT_CODEC = Codec.withAlternative(
++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.codec.apply(COMPONENT_CODEC)).codec(),
++ HOVER_EVENT_TYPE_CODEC.<HoverEvent<?>>dispatchMap("action", GET_HOVER_EVENT_TYPE, het -> het.legacyCodec.apply(COMPONENT_CODEC)).codec()
++ );
++
++ public static final MapCodec<Style> STYLE_MAP_CODEC = mapCodec((instance) -> {
++ return instance.group(
++ TEXT_COLOR_CODEC.optionalFieldOf("color").forGetter(nullableGetter(Style::color)),
++ Codec.BOOL.optionalFieldOf("bold").forGetter(decorationGetter(TextDecoration.BOLD)),
++ Codec.BOOL.optionalFieldOf("italic").forGetter(decorationGetter(TextDecoration.ITALIC)),
++ Codec.BOOL.optionalFieldOf("underlined").forGetter(decorationGetter(TextDecoration.UNDERLINED)),
++ Codec.BOOL.optionalFieldOf("strikethrough").forGetter(decorationGetter(TextDecoration.STRIKETHROUGH)),
++ Codec.BOOL.optionalFieldOf("obfuscated").forGetter(decorationGetter(TextDecoration.OBFUSCATED)),
++ CLICK_EVENT_CODEC.optionalFieldOf("clickEvent").forGetter(nullableGetter(Style::clickEvent)),
++ HOVER_EVENT_CODEC.optionalFieldOf("hoverEvent").forGetter(nullableGetter(Style::hoverEvent)),
++ Codec.STRING.optionalFieldOf("insertion").forGetter(nullableGetter(Style::insertion)),
++ KEY_CODEC.optionalFieldOf("font").forGetter(nullableGetter(Style::font))
++ ).apply(instance, (textColor, bold, italic, underlined, strikethrough, obfuscated, clickEvent, hoverEvent, insertion, font) -> {
++ return Style.style(builder -> {
++ textColor.ifPresent(builder::color);
++ bold.ifPresent(styleBooleanConsumer(builder, TextDecoration.BOLD));
++ italic.ifPresent(styleBooleanConsumer(builder, TextDecoration.ITALIC));
++ underlined.ifPresent(styleBooleanConsumer(builder, TextDecoration.UNDERLINED));
++ strikethrough.ifPresent(styleBooleanConsumer(builder, TextDecoration.STRIKETHROUGH));
++ obfuscated.ifPresent(styleBooleanConsumer(builder, TextDecoration.OBFUSCATED));
++ clickEvent.ifPresent(builder::clickEvent);
++ hoverEvent.ifPresent(builder::hoverEvent);
++ insertion.ifPresent(builder::insertion);
++ font.ifPresent(builder::font);
++ });
++ });
++ });
++ static Consumer<Boolean> styleBooleanConsumer(final Style.Builder builder, final TextDecoration decoration) {
++ return b -> builder.decoration(decoration, b);
++ }
++
++ static Function<Style, Optional<Boolean>> decorationGetter(final TextDecoration decoration) {
++ return style -> Optional.ofNullable(style.decoration(decoration) == TextDecoration.State.NOT_SET ? null : style.decoration(decoration) == TextDecoration.State.TRUE);
++ }
++
++ static <R, T> Function<R, Optional<T>> nullableGetter(final Function<R, @Nullable T> getter) {
++ return style -> Optional.ofNullable(getter.apply(style));
++ }
++
++ static final MapCodec<TextComponent> TEXT_COMPONENT_MAP_CODEC = mapCodec((instance) -> {
++ return instance.group(Codec.STRING.fieldOf("text").forGetter(TextComponent::content)).apply(instance, Component::text);
++ });
++ static final Codec<Object> PRIMITIVE_ARG_CODEC = ExtraCodecs.JAVA.validate(TranslatableContents::filterAllowedArguments);
++ static final Codec<TranslationArgument> ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, COMPONENT_CODEC).flatXmap((primitiveOrComponent) -> {
++ return primitiveOrComponent.map(o -> {
++ final TranslationArgument arg;
++ if (o instanceof String s) {
++ arg = component(text(s));
++ } else if (o instanceof Boolean bool) {
++ arg = bool(bool);
++ } else if (o instanceof Number num) {
++ arg = numeric(num);
++ } else {
++ return DataResult.error(() -> o + " is not a valid translation argument primitive");
++ }
++ return DataResult.success(arg);
++ }, component -> DataResult.success(component(component)));
++ }, translationArgument -> {
++ if (translationArgument.value() instanceof Number || translationArgument.value() instanceof Boolean) {
++ return DataResult.success(Either.left(translationArgument.value()));
++ }
++ final Component component = translationArgument.asComponent();
++ final @Nullable String collapsed = tryCollapseToString(component);
++ if (collapsed != null) {
++ return DataResult.success(Either.left(collapsed)); // attempt to collapse all text components to strings
++ }
++ return DataResult.success(Either.right(component));
++ });
++ static final MapCodec<TranslatableComponent> TRANSLATABLE_COMPONENT_MAP_CODEC = mapCodec((instance) -> {
++ return instance.group(
++ Codec.STRING.fieldOf("translate").forGetter(TranslatableComponent::key),
++ Codec.STRING.lenientOptionalFieldOf("fallback").forGetter(nullableGetter(TranslatableComponent::fallback)),
++ ARG_CODEC.listOf().optionalFieldOf("with").forGetter(c -> c.arguments().isEmpty() ? Optional.empty() : Optional.of(c.arguments()))
++ ).apply(instance, (key, fallback, components) -> {
++ return Component.translatable(key, components.orElse(Collections.emptyList())).fallback(fallback.orElse(null));
++ });
++ });
++
++ static final MapCodec<KeybindComponent> KEYBIND_COMPONENT_MAP_CODEC = KeybindContents.CODEC.xmap(k -> Component.keybind(k.getName()), k -> new KeybindContents(k.keybind()));
++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_INNER_MAP_CODEC = ScoreContents.INNER_CODEC.xmap(
++ s -> Component.score(s.name().map(SelectorPattern::pattern, Function.identity()), s.objective()),
++ s -> new ScoreContents(SelectorPattern.parse(s.name()).<Either<SelectorPattern, String>>map(Either::left).result().orElse(Either.right(s.name())), s.objective())
++ ); // TODO we might want to ask adventure for a nice way we can avoid parsing and flattening the SelectorPattern on every conversion.
++ static final MapCodec<ScoreComponent> SCORE_COMPONENT_MAP_CODEC = SCORE_COMPONENT_INNER_MAP_CODEC.fieldOf("score");
++ static final MapCodec<SelectorComponent> SELECTOR_COMPONENT_MAP_CODEC = mapCodec((instance) -> {
++ return instance.group(
++ Codec.STRING.fieldOf("selector").forGetter(SelectorComponent::pattern),
++ COMPONENT_CODEC.optionalFieldOf("separator").forGetter(nullableGetter(SelectorComponent::separator))
++ ).apply(instance, (selector, component) -> Component.selector(selector, component.orElse(null)));
++ });
++
++ interface NbtComponentDataSource {
++ NBTComponentBuilder<?, ?> builder();
++
++ DataSourceType<?> type();
++ }
++
++ record StorageDataSource(Key storage) implements NbtComponentDataSource {
++ @Override
++ public NBTComponentBuilder<?, ?> builder() {
++ return Component.storageNBT().storage(this.storage());
++ }
++
++ @Override
++ public DataSourceType<?> type() {
++ return STORAGE_DATA_SOURCE_TYPE;
++ }
++ }
++
++ record BlockDataSource(String posPattern) implements NbtComponentDataSource {
++ @Override
++ public NBTComponentBuilder<?, ?> builder() {
++ return Component.blockNBT().pos(BlockNBTComponent.Pos.fromString(this.posPattern));
++ }
++
++ @Override
++ public DataSourceType<?> type() {
++ return BLOCK_DATA_SOURCE_TYPE;
++ }
++ }
++
++ record EntityDataSource(String selectorPattern) implements NbtComponentDataSource {
++ @Override
++ public NBTComponentBuilder<?, ?> builder() {
++ return Component.entityNBT().selector(this.selectorPattern());
++ }
++
++ @Override
++ public DataSourceType<?> type() {
++ return ENTITY_DATA_SOURCE_TYPE;
++ }
++ }
++
++ static final DataSourceType<StorageDataSource> STORAGE_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(KEY_CODEC.fieldOf("storage").forGetter(StorageDataSource::storage)).apply(instance, StorageDataSource::new)), "storage");
++ static final DataSourceType<BlockDataSource> BLOCK_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("block").forGetter(BlockDataSource::posPattern)).apply(instance, BlockDataSource::new)), "block");
++ static final DataSourceType<EntityDataSource> ENTITY_DATA_SOURCE_TYPE = new DataSourceType<>(mapCodec((instance) -> instance.group(Codec.STRING.fieldOf("entity").forGetter(EntityDataSource::selectorPattern)).apply(instance, EntityDataSource::new)), "entity");
++
++ static final MapCodec<NbtComponentDataSource> NBT_COMPONENT_DATA_SOURCE_CODEC = ComponentSerialization.createLegacyComponentMatcher(new DataSourceType<?>[]{ENTITY_DATA_SOURCE_TYPE, BLOCK_DATA_SOURCE_TYPE, STORAGE_DATA_SOURCE_TYPE}, DataSourceType::codec, NbtComponentDataSource::type, "source");
++
++ record DataSourceType<D extends NbtComponentDataSource>(MapCodec<D> codec, String id) implements StringRepresentable {
++ @Override
++ public String getSerializedName() {
++ return this.id();
++ }
++ }
++
++ static final MapCodec<NBTComponent<?, ?>> NBT_COMPONENT_MAP_CODEC = mapCodec((instance) -> {
++ return instance.group(
++ Codec.STRING.fieldOf("nbt").forGetter(NBTComponent::nbtPath),
++ Codec.BOOL.lenientOptionalFieldOf("interpret", false).forGetter(NBTComponent::interpret),
++ COMPONENT_CODEC.lenientOptionalFieldOf("separator").forGetter(nullableGetter(NBTComponent::separator)),
++ NBT_COMPONENT_DATA_SOURCE_CODEC.forGetter(nbtComponent -> {
++ if (nbtComponent instanceof final EntityNBTComponent entityNBTComponent) {
++ return new EntityDataSource(entityNBTComponent.selector());
++ } else if (nbtComponent instanceof final BlockNBTComponent blockNBTComponent) {
++ return new BlockDataSource(blockNBTComponent.pos().asString());
++ } else if (nbtComponent instanceof final StorageNBTComponent storageNBTComponent) {
++ return new StorageDataSource(storageNBTComponent.storage());
++ } else {
++ throw new IllegalArgumentException(nbtComponent + " isn't a valid nbt component");
++ }
++ })
++ ).apply(instance, (nbtPath, interpret, separator, dataSource) -> {
++ return dataSource.builder().nbtPath(nbtPath).interpret(interpret).separator(separator.orElse(null)).build();
++ });
++ });
++
++ @SuppressWarnings("NonExtendableApiUsage")
++ record ComponentType<C extends Component>(MapCodec<C> codec, Predicate<Component> test, String id) implements StringRepresentable {
++ @Override
++ public String getSerializedName() {
++ return this.id;
++ }
++ }
++
++ static final ComponentType<TextComponent> PLAIN = new ComponentType<>(TEXT_COMPONENT_MAP_CODEC, TextComponent.class::isInstance, "text");
++ static final ComponentType<TranslatableComponent> TRANSLATABLE = new ComponentType<>(TRANSLATABLE_COMPONENT_MAP_CODEC, TranslatableComponent.class::isInstance, "translatable");
++ static final ComponentType<KeybindComponent> KEYBIND = new ComponentType<>(KEYBIND_COMPONENT_MAP_CODEC, KeybindComponent.class::isInstance, "keybind");
++ static final ComponentType<ScoreComponent> SCORE = new ComponentType<>(SCORE_COMPONENT_MAP_CODEC, ScoreComponent.class::isInstance, "score");
++ static final ComponentType<SelectorComponent> SELECTOR = new ComponentType<>(SELECTOR_COMPONENT_MAP_CODEC, SelectorComponent.class::isInstance, "selector");
++ static final ComponentType<NBTComponent<?, ?>> NBT = new ComponentType<>(NBT_COMPONENT_MAP_CODEC, NBTComponent.class::isInstance, "nbt");
++
++ static Codec<Component> createCodec(final Codec<Component> selfCodec) {
++ final ComponentType<?>[] types = new ComponentType<?>[]{PLAIN, TRANSLATABLE, KEYBIND, SCORE, SELECTOR, NBT};
++ final MapCodec<Component> legacyCodec = ComponentSerialization.createLegacyComponentMatcher(types, ComponentType::codec, component -> {
++ for (final ComponentType<?> type : types) {
++ if (type.test().test(component)) {
++ return type;
++ }
++ }
++ throw new IllegalStateException("Unexpected component type " + component);
++ }, "type");
++
++ final Codec<Component> directCodec = RecordCodecBuilder.create((instance) -> {
++ return instance.group(
++ legacyCodec.forGetter(identity()),
++ ExtraCodecs.nonEmptyList(selfCodec.listOf()).optionalFieldOf("extra", List.of()).forGetter(Component::children),
++ STYLE_MAP_CODEC.forGetter(Component::style)
++ ).apply(instance, (component, children, style) -> {
++ return component.style(style).children(children);
++ });
++ });
++
++ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), directCodec).xmap((stringOrListOrComponent) -> {
++ return stringOrListOrComponent.map((stringOrList) -> stringOrList.map(Component::text, AdventureCodecs::createFromList), identity());
++ }, (text) -> {
++ final @Nullable String string = tryCollapseToString(text);
++ return string != null ? Either.left(Either.left(string)) : Either.right(text);
++ });
++ }
++
++ public static @Nullable String tryCollapseToString(final Component component) {
++ if (component instanceof final TextComponent textComponent) {
++ if (component.children().isEmpty() && component.style().isEmpty()) {
++ return textComponent.content();
++ }
++ }
++ return null;
++ }
++
++ static Component createFromList(final List<? extends Component> components) {
++ Component component = components.get(0);
++ for (int i = 1; i < components.size(); i++) {
++ component = component.append(components.get(i));
++ }
++ return component;
++ }
++
++ private AdventureCodecs() {
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/AdventureComponent.java b/src/main/java/io/papermc/paper/adventure/AdventureComponent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c9d787f4e66f152b557229fdb1d9a3ac83a7d71f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/AdventureComponent.java
+@@ -0,0 +1,88 @@
++package io.papermc.paper.adventure;
++
++import java.util.List;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.TextComponent;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import net.minecraft.network.chat.ComponentContents;
++import net.minecraft.network.chat.MutableComponent;
++import net.minecraft.network.chat.Style;
++import net.minecraft.network.chat.contents.PlainTextContents;
++import net.minecraft.util.FormattedCharSequence;
++import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
++import org.jetbrains.annotations.Nullable;
++
++public final class AdventureComponent implements net.minecraft.network.chat.Component {
++ final Component adventure;
++ private net.minecraft.network.chat.@MonotonicNonNull Component vanilla;
++
++ public AdventureComponent(final Component adventure) {
++ this.adventure = adventure;
++ }
++
++ public net.minecraft.network.chat.Component deepConverted() {
++ net.minecraft.network.chat.Component vanilla = this.vanilla;
++ if (vanilla == null) {
++ vanilla = PaperAdventure.WRAPPER_AWARE_SERIALIZER.serialize(this.adventure);
++ this.vanilla = vanilla;
++ }
++ return vanilla;
++ }
++
++ public net.minecraft.network.chat.@Nullable Component deepConvertedIfPresent() {
++ return this.vanilla;
++ }
++
++ @Override
++ public Style getStyle() {
++ return this.deepConverted().getStyle();
++ }
++
++ @Override
++ public ComponentContents getContents() {
++ if (this.adventure instanceof TextComponent) {
++ return PlainTextContents.create(((TextComponent) this.adventure).content());
++ } else {
++ return this.deepConverted().getContents();
++ }
++ }
++
++ @Override
++ public String getString() {
++ return PlainTextComponentSerializer.plainText().serialize(this.adventure);
++ }
++
++ @Override
++ public List<net.minecraft.network.chat.Component> getSiblings() {
++ return this.deepConverted().getSiblings();
++ }
++
++ @Override
++ public MutableComponent plainCopy() {
++ return this.deepConverted().plainCopy();
++ }
++
++ @Override
++ public MutableComponent copy() {
++ return this.deepConverted().copy();
++ }
++
++ @Override
++ public FormattedCharSequence getVisualOrderText() {
++ return this.deepConverted().getVisualOrderText();
++ }
++
++ public Component adventure$component() {
++ return this.adventure;
++ }
++
++ @Override
++ public int hashCode() {
++ return this.deepConverted().hashCode();
++ }
++
++ @Override
++ public boolean equals(final Object obj) {
++ return this.deepConverted().equals(obj);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java b/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..23bd6d2d8fed5a3491e856f8b875456dd29f8aaf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/BossBarImplementationImpl.java
+@@ -0,0 +1,85 @@
++package io.papermc.paper.adventure;
++
++import com.google.common.collect.Collections2;
++import java.util.Set;
++import java.util.function.Function;
++import net.kyori.adventure.bossbar.BossBar;
++import net.kyori.adventure.bossbar.BossBarImplementation;
++import net.kyori.adventure.bossbar.BossBarViewer;
++import net.kyori.adventure.text.Component;
++import net.minecraft.network.protocol.game.ClientboundBossEventPacket;
++import net.minecraft.server.level.ServerBossEvent;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.BossEvent;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.NotNull;
++
++@SuppressWarnings("UnstableApiUsage")
++public final class BossBarImplementationImpl implements BossBar.Listener, BossBarImplementation {
++ private final BossBar bar;
++ private ServerBossEvent vanilla;
++
++ public BossBarImplementationImpl(final BossBar bar) {
++ this.bar = bar;
++ }
++
++ public void playerShow(final CraftPlayer player) {
++ if (this.vanilla == null) {
++ this.vanilla = new ServerBossEvent(
++ PaperAdventure.asVanilla(this.bar.name()),
++ PaperAdventure.asVanilla(this.bar.color()),
++ PaperAdventure.asVanilla(this.bar.overlay())
++ );
++ this.vanilla.adventure = this.bar;
++ this.bar.addListener(this);
++ }
++ this.vanilla.addPlayer(player.getHandle());
++ }
++
++ public void playerHide(final CraftPlayer player) {
++ if (this.vanilla != null) {
++ this.vanilla.removePlayer(player.getHandle());
++ if (this.vanilla.getPlayers().isEmpty()) {
++ this.bar.removeListener(this);
++ this.vanilla = null;
++ }
++ }
++ }
++
++ @Override
++ public void bossBarNameChanged(final @NonNull BossBar bar, final @NonNull Component oldName, final @NonNull Component newName) {
++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateNamePacket);
++ }
++
++ @Override
++ public void bossBarProgressChanged(final @NonNull BossBar bar, final float oldProgress, final float newProgress) {
++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateProgressPacket);
++ }
++
++ @Override
++ public void bossBarColorChanged(final @NonNull BossBar bar, final BossBar.@NonNull Color oldColor, final BossBar.@NonNull Color newColor) {
++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket);
++ }
++
++ @Override
++ public void bossBarOverlayChanged(final @NonNull BossBar bar, final BossBar.@NonNull Overlay oldOverlay, final BossBar.@NonNull Overlay newOverlay) {
++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdateStylePacket);
++ }
++
++ @Override
++ public void bossBarFlagsChanged(final @NonNull BossBar bar, final @NonNull Set<BossBar.Flag> flagsAdded, final @NonNull Set<BossBar.Flag> flagsRemoved) {
++ this.maybeBroadcast(ClientboundBossEventPacket::createUpdatePropertiesPacket);
++ }
++
++ @Override
++ public @NotNull Iterable<? extends BossBarViewer> viewers() {
++ return this.vanilla == null ? Set.of() : Collections2.transform(this.vanilla.getPlayers(), ServerPlayer::getBukkitEntity);
++ }
++
++ private void maybeBroadcast(final Function<BossEvent, ClientboundBossEventPacket> fn) {
++ if (this.vanilla != null) {
++ this.vanilla.broadcast(fn);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b9a64a40dbb025e34a3de81df1208de45df3cfcc
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+@@ -0,0 +1,376 @@
++package io.papermc.paper.adventure;
++
++import io.papermc.paper.chat.ChatRenderer;
++import io.papermc.paper.event.player.AbstractChatEvent;
++import io.papermc.paper.event.player.AsyncChatEvent;
++import io.papermc.paper.event.player.ChatEvent;
++import java.lang.reflect.Field;
++import java.lang.reflect.Modifier;
++import java.util.BitSet;
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Map;
++import java.util.Objects;
++import java.util.Set;
++import java.util.concurrent.ExecutionException;
++import java.util.function.Function;
++import net.kyori.adventure.audience.Audience;
++import net.kyori.adventure.audience.ForwardingAudience;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.minecraft.Optionull;
++import net.minecraft.Util;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.network.chat.ChatType;
++import net.minecraft.network.chat.OutgoingChatMessage;
++import net.minecraft.network.chat.PlayerChatMessage;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerPlayer;
++import org.bukkit.command.ConsoleCommandSender;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.bukkit.craftbukkit.util.LazyPlayerSet;
++import org.bukkit.craftbukkit.util.Waitable;
++import org.bukkit.entity.Player;
++import org.bukkit.event.Event;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.player.AsyncPlayerChatEvent;
++import org.bukkit.event.player.PlayerChatEvent;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.intellij.lang.annotations.Subst;
++
++import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;
++
++@DefaultQualifier(NonNull.class)
++public final class ChatProcessor {
++ static final ResourceKey<ChatType> PAPER_RAW = ResourceKey.create(Registries.CHAT_TYPE, ResourceLocation.fromNamespaceAndPath(ResourceLocation.PAPER_NAMESPACE, "raw"));
++ static final String DEFAULT_LEGACY_FORMAT = "<%1$s> %2$s"; // copied from PlayerChatEvent/AsyncPlayerChatEvent
++ final MinecraftServer server;
++ final ServerPlayer player;
++ final PlayerChatMessage message;
++ final boolean async;
++ final String craftbukkit$originalMessage;
++ final Component paper$originalMessage;
++ final OutgoingChatMessage outgoing;
++
++ static final int MESSAGE_CHANGED = 1;
++ static final int FORMAT_CHANGED = 2;
++ static final int SENDER_CHANGED = 3; // Not used
++ private final BitSet flags = new BitSet(3);
++
++ public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final PlayerChatMessage message, final boolean async) {
++ this.server = server;
++ this.player = player;
++ this.message = message;
++ this.async = async;
++ this.craftbukkit$originalMessage = message.unsignedContent() != null ? LegacyComponentSerializer.legacySection().serialize(PaperAdventure.asAdventure(message.unsignedContent())) : message.signedContent();
++ this.paper$originalMessage = PaperAdventure.asAdventure(this.message.decoratedContent());
++ this.outgoing = OutgoingChatMessage.create(this.message);
++ }
++
++ @SuppressWarnings("deprecated")
++ public void process() {
++ final boolean listenersOnAsyncEvent = canYouHearMe(AsyncPlayerChatEvent.getHandlerList());
++ final boolean listenersOnSyncEvent = canYouHearMe(PlayerChatEvent.getHandlerList());
++ if (listenersOnAsyncEvent || listenersOnSyncEvent) {
++ final CraftPlayer player = this.player.getBukkitEntity();
++ final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server));
++ this.post(ae);
++ if (listenersOnSyncEvent) {
++ final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients());
++ se.setCancelled(ae.isCancelled()); // propagate cancelled state
++ this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
++ @Override
++ protected Void evaluate() {
++ ChatProcessor.this.post(se);
++ return null;
++ }
++ });
++ this.readLegacyModifications(se.getMessage(), se.getFormat(), se.getPlayer());
++ this.processModern(
++ this.modernRenderer(se.getFormat()),
++ this.viewersFromLegacy(se.getRecipients()),
++ this.modernMessage(se.getMessage()),
++ se.getPlayer(),
++ se.isCancelled()
++ );
++ } else {
++ this.readLegacyModifications(ae.getMessage(), ae.getFormat(), ae.getPlayer());
++ this.processModern(
++ this.modernRenderer(ae.getFormat()),
++ this.viewersFromLegacy(ae.getRecipients()),
++ this.modernMessage(ae.getMessage()),
++ ae.getPlayer(),
++ ae.isCancelled()
++ );
++ }
++ } else {
++ this.processModern(
++ defaultRenderer(),
++ new LazyChatAudienceSet(this.server),
++ this.paper$originalMessage,
++ this.player.getBukkitEntity(),
++ false
++ );
++ }
++ }
++
++ private ChatRenderer modernRenderer(final String format) {
++ if (this.flags.get(FORMAT_CHANGED)) {
++ return legacyRenderer(format);
++ } else {
++ return defaultRenderer();
++ }
++ }
++
++ private Component modernMessage(final String legacyMessage) {
++ if (this.flags.get(MESSAGE_CHANGED)) {
++ return legacySection().deserialize(legacyMessage);
++ } else {
++ return this.paper$originalMessage;
++ }
++ }
++
++ private void readLegacyModifications(final String message, final String format, final Player playerSender) {
++ this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage));
++ this.flags.set(FORMAT_CHANGED, !format.equals(DEFAULT_LEGACY_FORMAT));
++ this.flags.set(SENDER_CHANGED, playerSender != this.player.getBukkitEntity());
++ }
++
++ private void processModern(final ChatRenderer renderer, final Set<Audience> viewers, final Component message, final Player player, final boolean cancelled) {
++ final PlayerChatMessage.AdventureView signedMessage = this.message.adventureView();
++ final AsyncChatEvent ae = new AsyncChatEvent(this.async, player, viewers, renderer, message, this.paper$originalMessage, signedMessage);
++ ae.setCancelled(cancelled); // propagate cancelled state
++ this.post(ae);
++ final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList());
++ if (listenersOnSyncEvent) {
++ this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
++ @Override
++ protected Void evaluate() {
++ final ChatEvent se = new ChatEvent(player, ae.viewers(), ae.renderer(), ae.message(), ChatProcessor.this.paper$originalMessage/*, ae.usePreviewComponent()*/, signedMessage);
++ se.setCancelled(ae.isCancelled()); // propagate cancelled state
++ ChatProcessor.this.post(se);
++ ChatProcessor.this.readModernModifications(se, renderer);
++ ChatProcessor.this.complete(se);
++ return null;
++ }
++ });
++ } else {
++ this.readModernModifications(ae, renderer);
++ this.complete(ae);
++ }
++ }
++
++ private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) {
++ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.paper$originalMessage));
++ if (originalRenderer != chatEvent.renderer()) { // don't set to false if it hasn't changed
++ this.flags.set(FORMAT_CHANGED, true);
++ }
++ }
++
++ private void complete(final AbstractChatEvent event) {
++ if (event.isCancelled()) {
++ return;
++ }
++
++ final CraftPlayer player = ((CraftPlayer) event.getPlayer());
++ final Component displayName = displayName(player);
++ final Component message = event.message();
++ final ChatRenderer renderer = event.renderer();
++
++ final Set<Audience> viewers = event.viewers();
++ final ResourceKey<ChatType> chatTypeKey = renderer instanceof ChatRenderer.Default ? ChatType.CHAT : PAPER_RAW;
++ final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level().registryAccess(), PaperAdventure.asVanilla(displayName(player)));
++
++ OutgoingChat outgoingChat = viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy() ? new ServerOutgoingChat() : new ViewersOutgoingChat();
++ if (this.flags.get(FORMAT_CHANGED)) {
++ if (renderer instanceof ChatRenderer.ViewerUnaware unaware) {
++ outgoingChat.sendFormatChangedViewerUnaware(player, PaperAdventure.asVanilla(unaware.render(player, displayName, message)), viewers, chatType);
++ } else {
++ outgoingChat.sendFormatChangedViewerAware(player, displayName, message, renderer, viewers, chatType);
++ }
++ } else if (this.flags.get(MESSAGE_CHANGED)) {
++ if (!(renderer instanceof ChatRenderer.ViewerUnaware unaware)) {
++ throw new IllegalStateException("BUG: This should be a ViewerUnaware renderer at this point");
++ }
++ final Component renderedComponent = chatTypeKey == ChatType.CHAT ? message : unaware.render(player, displayName, message);
++ outgoingChat.sendMessageChanged(player, PaperAdventure.asVanilla(renderedComponent), viewers, chatType);
++ } else {
++ outgoingChat.sendOriginal(player, viewers, chatType);
++ }
++ }
++
++ interface OutgoingChat {
++ default void sendFormatChangedViewerUnaware(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
++ this.sendMessageChanged(player, renderedMessage, viewers, chatType);
++ }
++
++ void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType);
++
++ void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType);
++
++ void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType);
++ }
++
++ final class ServerOutgoingChat implements OutgoingChat {
++ @Override
++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType, viewer -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, viewer)));
++ }
++
++ @Override
++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message.withUnsignedContent(renderedMessage), ChatProcessor.this.player, chatType);
++ }
++
++ @Override
++ public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType);
++ }
++ }
++
++ final class ViewersOutgoingChat implements OutgoingChat {
++ @Override
++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, chatType, v -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v)));
++ }
++
++ @Override
++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, chatType, $ -> renderedMessage);
++ }
++
++ @Override
++ public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, chatType, null);
++ }
++
++ private void broadcastToViewers(Collection<Audience> viewers, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
++ for (Audience viewer : viewers) {
++ if (acceptsNative(viewer)) {
++ this.sendNative(viewer, chatType, msgFunction);
++ } else {
++ final net.minecraft.network.chat.@Nullable Component unsigned = Optionull.map(msgFunction, f -> f.apply(viewer));
++ final PlayerChatMessage msg = unsigned == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(unsigned);
++ viewer.sendMessage(msg.adventureView(), this.adventure(chatType));
++ }
++ }
++ }
++
++ private static final Map<String, net.kyori.adventure.chat.ChatType> BUILT_IN_CHAT_TYPES = Util.make(() -> {
++ final Map<String, net.kyori.adventure.chat.ChatType> map = new HashMap<>();
++ for (final Field declaredField : net.kyori.adventure.chat.ChatType.class.getDeclaredFields()) {
++ if (Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().equals(ChatType.class)) {
++ try {
++ final net.kyori.adventure.chat.ChatType type = (net.kyori.adventure.chat.ChatType) declaredField.get(null);
++ map.put(type.key().asString(), type);
++ } catch (final ReflectiveOperationException ignore) {
++ }
++ }
++ }
++ return map;
++ });
++
++ private net.kyori.adventure.chat.ChatType.Bound adventure(ChatType.Bound chatType) {
++ @Subst("key:value") final String stringKey = Objects.requireNonNull(
++ chatType.chatType().unwrapKey().orElseThrow().location(),
++ () -> "No key for '%s' in CHAT_TYPE registry.".formatted(chatType)
++ ).toString();
++ net.kyori.adventure.chat.@Nullable ChatType adventure = BUILT_IN_CHAT_TYPES.get(stringKey);
++ if (adventure == null) {
++ adventure = net.kyori.adventure.chat.ChatType.chatType(Key.key(stringKey));
++ }
++ return adventure.bind(
++ PaperAdventure.asAdventure(chatType.name()),
++ chatType.targetName().map(PaperAdventure::asAdventure).orElse(null)
++ );
++ }
++
++ private static boolean acceptsNative(final Audience viewer) {
++ if (viewer instanceof Player || viewer instanceof ConsoleCommandSender) {
++ return true;
++ }
++ if (viewer instanceof ForwardingAudience.Single single) {
++ return acceptsNative(single.audience());
++ }
++ return false;
++ }
++
++ private void sendNative(final Audience viewer, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
++ if (viewer instanceof ConsoleCommandSender) {
++ this.sendToServer(chatType, msgFunction);
++ } else if (viewer instanceof CraftPlayer craftPlayer) {
++ craftPlayer.getHandle().sendChatMessage(ChatProcessor.this.outgoing, ChatProcessor.this.player.shouldFilterMessageTo(craftPlayer.getHandle()), chatType, Optionull.map(msgFunction, f -> f.apply(viewer)));
++ } else if (viewer instanceof ForwardingAudience.Single single) {
++ this.sendNative(single.audience(), chatType, msgFunction);
++ } else {
++ throw new IllegalStateException("Should only be a Player or Console or ForwardingAudience.Single pointing to one!");
++ }
++ }
++
++ private void sendToServer(final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
++ final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console));
++ ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure");
++ }
++ }
++
++ private Set<Audience> viewersFromLegacy(final Set<Player> recipients) {
++ if (recipients instanceof LazyPlayerSet lazyPlayerSet && lazyPlayerSet.isLazy()) {
++ return new LazyChatAudienceSet(this.server);
++ }
++ final HashSet<Audience> viewers = new HashSet<>(recipients);
++ viewers.add(this.server.console);
++ return viewers;
++ }
++
++ static String legacyDisplayName(final CraftPlayer player) {
++ return player.getDisplayName();
++ }
++
++ static Component displayName(final CraftPlayer player) {
++ return player.displayName();
++ }
++
++ private static ChatRenderer.Default defaultRenderer() {
++ return (ChatRenderer.Default) ChatRenderer.defaultRenderer();
++ }
++
++ private static ChatRenderer legacyRenderer(final String format) {
++ if (DEFAULT_LEGACY_FORMAT.equals(format)) {
++ return defaultRenderer();
++ }
++ return ChatRenderer.viewerUnaware((player, sourceDisplayName, message) -> legacySection().deserialize(legacyFormat(format, player, legacySection().serialize(message))));
++ }
++
++ static String legacyFormat(final String format, Player player, String message) {
++ return String.format(format, legacyDisplayName((CraftPlayer) player), message);
++ }
++
++ private void queueIfAsyncOrRunImmediately(final Waitable<Void> waitable) {
++ if (this.async) {
++ this.server.processQueue.add(waitable);
++ } else {
++ waitable.run();
++ }
++ try {
++ waitable.get();
++ } catch (final InterruptedException e) {
++ Thread.currentThread().interrupt(); // tag, you're it
++ } catch (final ExecutionException e) {
++ throw new RuntimeException("Exception processing chat", e.getCause());
++ }
++ }
++
++ private void post(final Event event) {
++ this.server.server.getPluginManager().callEvent(event);
++ }
++
++ static boolean canYouHearMe(final HandlerList handlers) {
++ return handlers.getRegisteredListeners().length > 0;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/DisplayNames.java b/src/main/java/io/papermc/paper/adventure/DisplayNames.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d496a9a6ad229e42f1c44e31eafa6974b9faced5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/DisplayNames.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.adventure;
++
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.minecraft.server.level.ServerPlayer;
++import org.bukkit.ChatColor;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++
++public final class DisplayNames {
++ private DisplayNames() {
++ }
++
++ public static String getLegacy(final CraftPlayer player) {
++ return getLegacy(player.getHandle());
++ }
++
++ @SuppressWarnings("deprecation") // Valid suppress due to supporting legacy display name formatting
++ public static String getLegacy(final ServerPlayer player) {
++ final String legacy = player.displayName;
++ if (legacy != null) {
++ // thank you for being worse than wet socks, Bukkit
++ return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName) + ChatColor.getLastColors(player.displayName);
++ }
++ return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0848ad9c7a6d386f0219b75df1ae4d08ba23aa59
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java
+@@ -0,0 +1,55 @@
++package io.papermc.paper.adventure;
++
++import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;
++import io.papermc.paper.event.player.AsyncChatDecorateEvent;
++import java.util.concurrent.CompletableFuture;
++import net.minecraft.commands.CommandSourceStack;
++import net.minecraft.network.chat.ChatDecorator;
++import net.minecraft.network.chat.Component;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerPlayer;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class ImprovedChatDecorator implements ChatDecorator {
++ private final MinecraftServer server;
++
++ public ImprovedChatDecorator(final MinecraftServer server) {
++ this.server = server;
++ }
++
++ @Override
++ public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final Component message) {
++ return decorate(this.server, sender, null, message);
++ }
++
++ @Override
++ public CompletableFuture<Component> decorate(final @Nullable ServerPlayer sender, final @Nullable CommandSourceStack commandSourceStack, final Component message) {
++ return decorate(this.server, sender, commandSourceStack, message);
++ }
++
++ private static CompletableFuture<Component> decorate(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final Component originalMessage) {
++ return CompletableFuture.supplyAsync(() -> {
++ final net.kyori.adventure.text.Component initialResult = PaperAdventure.asAdventure(originalMessage);
++
++ final @Nullable CraftPlayer craftPlayer = player == null ? null : player.getBukkitEntity();
++
++ final AsyncChatDecorateEvent event;
++ if (commandSourceStack != null) {
++ // TODO more command decorate context
++ event = new AsyncChatCommandDecorateEvent(craftPlayer, initialResult);
++ } else {
++ event = new AsyncChatDecorateEvent(craftPlayer, initialResult);
++ }
++
++ if (event.callEvent()) {
++ return PaperAdventure.asVanilla(event.result());
++ }
++
++ return originalMessage;
++ }, server.chatExecutor);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2fd6c3e65354071af71c7d8ebb97b559b6e105ce
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java
+@@ -0,0 +1,26 @@
++package io.papermc.paper.adventure;
++
++import java.util.HashSet;
++import java.util.Set;
++import net.kyori.adventure.audience.Audience;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.util.LazyHashSet;
++import org.bukkit.craftbukkit.util.LazyPlayerSet;
++import org.bukkit.entity.Player;
++
++final class LazyChatAudienceSet extends LazyHashSet<Audience> {
++ private final MinecraftServer server;
++
++ public LazyChatAudienceSet(final MinecraftServer server) {
++ this.server = server;
++ }
++
++ @Override
++ protected Set<Audience> makeReference() {
++ final Set<Player> playerSet = LazyPlayerSet.makePlayerSet(this.server);
++ final HashSet<Audience> audiences = new HashSet<>(playerSet);
++ audiences.add(Bukkit.getConsoleSender());
++ return audiences;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8ec506a1ae40f2e4b01af9b34a0b98be8653b460
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
+@@ -0,0 +1,505 @@
++package io.papermc.paper.adventure;
++
++import com.mojang.brigadier.StringReader;
++import com.mojang.brigadier.exceptions.CommandSyntaxException;
++import com.mojang.serialization.JavaOps;
++import io.netty.util.AttributeKey;
++import java.io.IOException;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Optional;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.function.BiConsumer;
++import java.util.regex.Matcher;
++import java.util.regex.Pattern;
++import java.util.stream.StreamSupport;
++import net.kyori.adventure.bossbar.BossBar;
++import net.kyori.adventure.inventory.Book;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.nbt.api.BinaryTagHolder;
++import net.kyori.adventure.sound.Sound;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.TranslatableComponent;
++import net.kyori.adventure.text.TranslationArgument;
++import net.kyori.adventure.text.event.DataComponentValue;
++import net.kyori.adventure.text.event.DataComponentValueConverterRegistry;
++import net.kyori.adventure.text.flattener.ComponentFlattener;
++import net.kyori.adventure.text.format.Style;
++import net.kyori.adventure.text.format.TextColor;
++import net.kyori.adventure.text.serializer.ComponentSerializer;
++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
++import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import net.kyori.adventure.translation.GlobalTranslator;
++import net.kyori.adventure.translation.TranslationRegistry;
++import net.kyori.adventure.translation.Translator;
++import net.kyori.adventure.util.Codec;
++import net.minecraft.ChatFormatting;
++import net.minecraft.commands.CommandSourceStack;
++import net.minecraft.core.Holder;
++import net.minecraft.core.component.DataComponentPatch;
++import net.minecraft.core.component.DataComponentType;
++import net.minecraft.core.component.DataComponents;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.locale.Language;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.nbt.TagParser;
++import net.minecraft.network.chat.ComponentUtils;
++import net.minecraft.network.protocol.Packet;
++import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket;
++import net.minecraft.network.protocol.game.ClientboundSoundPacket;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.network.Filterable;
++import net.minecraft.sounds.SoundEvent;
++import net.minecraft.sounds.SoundSource;
++import net.minecraft.world.BossEvent;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.item.ItemStack;
++import net.minecraft.world.item.component.WrittenBookContent;
++import org.bukkit.command.CommandSender;
++import org.bukkit.craftbukkit.CraftRegistry;
++import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
++import org.bukkit.craftbukkit.entity.CraftEntity;
++import org.intellij.lang.annotations.Subst;
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import static java.util.Objects.requireNonNull;
++
++public final class PaperAdventure {
++ private static final Pattern LOCALIZATION_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?s");
++ public static final ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder()
++ .complexMapper(TranslatableComponent.class, (translatable, consumer) -> {
++ if (!Language.getInstance().has(translatable.key())) {
++ for (final Translator source : GlobalTranslator.translator().sources()) {
++ if (source instanceof TranslationRegistry registry && registry.contains(translatable.key())) {
++ consumer.accept(GlobalTranslator.render(translatable, Locale.US));
++ return;
++ }
++ }
++ }
++ final @Nullable String fallback = translatable.fallback();
++ final @NotNull String translated = Language.getInstance().getOrDefault(translatable.key(), fallback != null ? fallback : translatable.key());
++
++ final Matcher matcher = LOCALIZATION_PATTERN.matcher(translated);
++ final List<TranslationArgument> args = translatable.arguments();
++ int argPosition = 0;
++ int lastIdx = 0;
++ while (matcher.find()) {
++ // append prior
++ if (lastIdx < matcher.start()) {
++ consumer.accept(Component.text(translated.substring(lastIdx, matcher.start())));
++ }
++ lastIdx = matcher.end();
++
++ final @Nullable String argIdx = matcher.group(1);
++ // calculate argument position
++ if (argIdx != null) {
++ try {
++ final int idx = Integer.parseInt(argIdx) - 1;
++ if (idx < args.size()) {
++ consumer.accept(args.get(idx).asComponent());
++ }
++ } catch (final NumberFormatException ex) {
++ // ignore, drop the format placeholder
++ }
++ } else {
++ final int idx = argPosition++;
++ if (idx < args.size()) {
++ consumer.accept(args.get(idx).asComponent());
++ }
++ }
++ }
++
++ // append tail
++ if (lastIdx < translated.length()) {
++ consumer.accept(Component.text(translated.substring(lastIdx)));
++ }
++ })
++ .build();
++ public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger
++ @Deprecated
++ public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build();
++ public static final Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() {
++ @Override
++ public @NotNull Tag decode(final @NotNull String encoded) throws CommandSyntaxException {
++ return new TagParser(new StringReader(encoded)).readValue();
++ }
++
++ @Override
++ public @NotNull String encode(final @NotNull Tag decoded) {
++ return decoded.toString();
++ }
++ };
++ public static final ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer(() -> CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE));
++
++ private PaperAdventure() {
++ }
++
++ // Key
++
++ public static Key asAdventure(final ResourceLocation key) {
++ return Key.key(key.getNamespace(), key.getPath());
++ }
++
++ public static ResourceLocation asVanilla(final Key key) {
++ return ResourceLocation.fromNamespaceAndPath(key.namespace(), key.value());
++ }
++
++ public static <T> ResourceKey<T> asVanilla(
++ final ResourceKey<? extends net.minecraft.core.Registry<T>> registry,
++ final Key key
++ ) {
++ return ResourceKey.create(registry, asVanilla(key));
++ }
++
++ public static Key asAdventureKey(final ResourceKey<?> key) {
++ return asAdventure(key.location());
++ }
++
++ public static @Nullable ResourceLocation asVanillaNullable(final Key key) {
++ if (key == null) {
++ return null;
++ }
++ return asVanilla(key);
++ }
++
++ public static Holder<SoundEvent> resolveSound(final Key key) {
++ ResourceLocation id = asVanilla(key);
++ Optional<Holder.Reference<SoundEvent>> vanilla = BuiltInRegistries.SOUND_EVENT.get(id);
++ if (vanilla.isPresent()) {
++ return vanilla.get();
++ }
++
++ // sound is not known so not in the registry but might be used by the client with a resource pack
++ return Holder.direct(SoundEvent.createVariableRangeEvent(id));
++ }
++
++ // Component
++
++ public static @NotNull Component asAdventure(@Nullable final net.minecraft.network.chat.Component component) {
++ return component == null ? Component.empty() : WRAPPER_AWARE_SERIALIZER.deserialize(component);
++ }
++
++ public static ArrayList<Component> asAdventure(final List<? extends net.minecraft.network.chat.Component> vanillas) {
++ final ArrayList<Component> adventures = new ArrayList<>(vanillas.size());
++ for (final net.minecraft.network.chat.Component vanilla : vanillas) {
++ adventures.add(asAdventure(vanilla));
++ }
++ return adventures;
++ }
++
++ public static ArrayList<Component> asAdventureFromJson(final List<String> jsonStrings) {
++ final ArrayList<Component> adventures = new ArrayList<>(jsonStrings.size());
++ for (final String json : jsonStrings) {
++ adventures.add(GsonComponentSerializer.gson().deserialize(json));
++ }
++ return adventures;
++ }
++
++ public static List<String> asJson(final List<? extends Component> adventures) {
++ final List<String> jsons = new ArrayList<>(adventures.size());
++ for (final Component component : adventures) {
++ jsons.add(GsonComponentSerializer.gson().serialize(component));
++ }
++ return jsons;
++ }
++
++ public static net.minecraft.network.chat.@NotNull Component asVanillaNullToEmpty(final @Nullable Component component) {
++ if (component == null) return net.minecraft.network.chat.CommonComponents.EMPTY;
++ return asVanilla(component);
++ }
++
++ @Contract("null -> null; !null -> !null")
++ public static net.minecraft.network.chat.Component asVanilla(final @Nullable Component component) {
++ if (component == null) return null;
++ if (true) return new AdventureComponent(component);
++ return WRAPPER_AWARE_SERIALIZER.serialize(component);
++ }
++
++ public static List<net.minecraft.network.chat.Component> asVanilla(final List<? extends Component> adventures) {
++ final List<net.minecraft.network.chat.Component> vanillas = new ArrayList<>(adventures.size());
++ for (final Component adventure : adventures) {
++ vanillas.add(asVanilla(adventure));
++ }
++ return vanillas;
++ }
++
++ public static String asJsonString(final Component component, final Locale locale) {
++ return GsonComponentSerializer.gson().serialize(translated(component, locale));
++ }
++
++ public static boolean hasAnyTranslations() {
++ return StreamSupport.stream(GlobalTranslator.translator().sources().spliterator(), false)
++ .anyMatch(t -> t.hasAnyTranslations().toBooleanOrElse(true));
++ }
++
++ private static final Map<Locale, com.mojang.serialization.Codec<Component>> LOCALIZED_CODECS = new ConcurrentHashMap<>();
++
++ public static com.mojang.serialization.Codec<Component> localizedCodec(final @Nullable Locale l) {
++ if (l == null) {
++ return AdventureCodecs.COMPONENT_CODEC;
++ }
++ return LOCALIZED_CODECS.computeIfAbsent(l, locale -> AdventureCodecs.COMPONENT_CODEC.xmap(
++ component -> component, // decode
++ component -> translated(component, locale) // encode
++ ));
++ }
++
++ public static String asPlain(final Component component, final Locale locale) {
++ return PlainTextComponentSerializer.plainText().serialize(translated(component, locale));
++ }
++
++ private static Component translated(final Component component, final Locale locale) {
++ //noinspection ConstantValue
++ return GlobalTranslator.render(
++ component,
++ // play it safe
++ locale != null
++ ? locale
++ : Locale.US
++ );
++ }
++
++ public static Component resolveWithContext(final @NotNull Component component, final @Nullable CommandSender context, final @Nullable org.bukkit.entity.Entity scoreboardSubject, final boolean bypassPermissions) throws IOException {
++ final CommandSourceStack css = context != null ? VanillaCommandWrapper.getListener(context) : null;
++ Boolean previous = null;
++ if (css != null && bypassPermissions) {
++ previous = css.bypassSelectorPermissions;
++ css.bypassSelectorPermissions = true;
++ }
++ try {
++ return asAdventure(ComponentUtils.updateForEntity(css, asVanilla(component), scoreboardSubject == null ? null : ((CraftEntity) scoreboardSubject).getHandle(), 0));
++ } catch (final CommandSyntaxException e) {
++ throw new IOException(e);
++ } finally {
++ if (css != null && previous != null) {
++ css.bypassSelectorPermissions = previous;
++ }
++ }
++ }
++
++ // BossBar
++
++ public static BossEvent.BossBarColor asVanilla(final BossBar.Color color) {
++ return switch (color) {
++ case PINK -> BossEvent.BossBarColor.PINK;
++ case BLUE -> BossEvent.BossBarColor.BLUE;
++ case RED -> BossEvent.BossBarColor.RED;
++ case GREEN -> BossEvent.BossBarColor.GREEN;
++ case YELLOW -> BossEvent.BossBarColor.YELLOW;
++ case PURPLE -> BossEvent.BossBarColor.PURPLE;
++ case WHITE -> BossEvent.BossBarColor.WHITE;
++ };
++ }
++
++ public static BossBar.Color asAdventure(final BossEvent.BossBarColor color) {
++ return switch (color) {
++ case PINK -> BossBar.Color.PINK;
++ case BLUE -> BossBar.Color.BLUE;
++ case RED -> BossBar.Color.RED;
++ case GREEN -> BossBar.Color.GREEN;
++ case YELLOW -> BossBar.Color.YELLOW;
++ case PURPLE -> BossBar.Color.PURPLE;
++ case WHITE -> BossBar.Color.WHITE;
++ };
++ }
++
++ public static BossEvent.BossBarOverlay asVanilla(final BossBar.Overlay overlay) {
++ return switch (overlay) {
++ case PROGRESS -> BossEvent.BossBarOverlay.PROGRESS;
++ case NOTCHED_6 -> BossEvent.BossBarOverlay.NOTCHED_6;
++ case NOTCHED_10 -> BossEvent.BossBarOverlay.NOTCHED_10;
++ case NOTCHED_12 -> BossEvent.BossBarOverlay.NOTCHED_12;
++ case NOTCHED_20 -> BossEvent.BossBarOverlay.NOTCHED_20;
++ };
++ }
++
++ public static BossBar.Overlay asAdventure(final BossEvent.BossBarOverlay overlay) {
++ return switch (overlay) {
++ case PROGRESS -> BossBar.Overlay.PROGRESS;
++ case NOTCHED_6 -> BossBar.Overlay.NOTCHED_6;
++ case NOTCHED_10 -> BossBar.Overlay.NOTCHED_10;
++ case NOTCHED_12 -> BossBar.Overlay.NOTCHED_12;
++ case NOTCHED_20 -> BossBar.Overlay.NOTCHED_20;
++ };
++ }
++
++ public static void setFlag(final BossBar bar, final BossBar.Flag flag, final boolean value) {
++ if (value) {
++ bar.addFlag(flag);
++ } else {
++ bar.removeFlag(flag);
++ }
++ }
++
++ // Book
++
++ public static ItemStack asItemStack(final Book book, final Locale locale) {
++ final ItemStack item = new ItemStack(net.minecraft.world.item.Items.WRITTEN_BOOK, 1);
++ item.set(DataComponents.WRITTEN_BOOK_CONTENT, new WrittenBookContent(
++ Filterable.passThrough(validateField(asPlain(book.title(), locale), WrittenBookContent.TITLE_MAX_LENGTH, "title")),
++ asPlain(book.author(), locale),
++ 0,
++ book.pages().stream().map(c -> Filterable.passThrough(PaperAdventure.asVanilla(c))).toList(), // TODO should we validate legnth?
++ false
++ ));
++ return item;
++ }
++
++ private static String validateField(final String content, final int length, final String name) {
++ final int actual = content.length();
++ if (actual > length) {
++ throw new IllegalArgumentException("Field '" + name + "' has a maximum length of " + length + " but was passed '" + content + "', which was " + actual + " characters long.");
++ }
++ return content;
++ }
++
++ // Sounds
++
++ public static SoundSource asVanilla(final Sound.Source source) {
++ return switch (source) {
++ case MASTER -> SoundSource.MASTER;
++ case MUSIC -> SoundSource.MUSIC;
++ case RECORD -> SoundSource.RECORDS;
++ case WEATHER -> SoundSource.WEATHER;
++ case BLOCK -> SoundSource.BLOCKS;
++ case HOSTILE -> SoundSource.HOSTILE;
++ case NEUTRAL -> SoundSource.NEUTRAL;
++ case PLAYER -> SoundSource.PLAYERS;
++ case AMBIENT -> SoundSource.AMBIENT;
++ case VOICE -> SoundSource.VOICE;
++ };
++ }
++
++ public static @Nullable SoundSource asVanillaNullable(final Sound.@Nullable Source source) {
++ if (source == null) {
++ return null;
++ }
++ return asVanilla(source);
++ }
++
++ public static Packet<?> asSoundPacket(final Sound sound, final double x, final double y, final double z, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) {
++ final ResourceLocation name = asVanilla(sound.name());
++ final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name);
++ final SoundSource source = asVanilla(sound.source());
++
++ final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name)));
++ final Packet<?> packet = new ClientboundSoundPacket(soundEventHolder, source, x, y, z, sound.volume(), sound.pitch(), seed);
++ if (packetConsumer != null) {
++ packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume()));
++ }
++ return packet;
++ }
++
++ public static Packet<?> asSoundPacket(final Sound sound, final Entity emitter, final long seed, @Nullable BiConsumer<Packet<?>, Float> packetConsumer) {
++ final ResourceLocation name = asVanilla(sound.name());
++ final Optional<SoundEvent> soundEvent = BuiltInRegistries.SOUND_EVENT.getOptional(name);
++ final SoundSource source = asVanilla(sound.source());
++
++ final Holder<SoundEvent> soundEventHolder = soundEvent.map(BuiltInRegistries.SOUND_EVENT::wrapAsHolder).orElseGet(() -> Holder.direct(SoundEvent.createVariableRangeEvent(name)));
++ final Packet<?> packet = new ClientboundSoundEntityPacket(soundEventHolder, source, emitter, sound.volume(), sound.pitch(), seed);
++ if (packetConsumer != null) {
++ packetConsumer.accept(packet, soundEventHolder.value().getRange(sound.volume()));
++ }
++ return packet;
++ }
++
++ // NBT
++
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ public static Map<Key, ? extends DataComponentValue> asAdventure(
++ final DataComponentPatch patch
++ ) {
++ if (patch.isEmpty()) {
++ return Collections.emptyMap();
++ }
++ final Map<Key, DataComponentValue> map = new HashMap<>();
++ for (final Map.Entry<DataComponentType<?>, Optional<?>> entry : patch.entrySet()) {
++ if (entry.getKey().isTransient()) continue;
++ @Subst("key:value") final String typeKey = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(entry.getKey())).toString();
++ if (entry.getValue().isEmpty()) {
++ map.put(Key.key(typeKey), DataComponentValue.removed());
++ } else {
++ map.put(Key.key(typeKey), new DataComponentValueImpl(entry.getKey().codec(), entry.getValue().get()));
++ }
++ }
++ return map;
++ }
++
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ public static DataComponentPatch asVanilla(final Map<? extends Key, ? extends DataComponentValue> map) {
++ if (map.isEmpty()) {
++ return DataComponentPatch.EMPTY;
++ }
++ final DataComponentPatch.Builder builder = DataComponentPatch.builder();
++ map.forEach((key, dataComponentValue) -> {
++ final DataComponentType<?> type = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(asVanilla(key)));
++ if (dataComponentValue instanceof DataComponentValue.Removed) {
++ builder.remove(type);
++ return;
++ }
++ final DataComponentValueImpl<?> converted = DataComponentValueConverterRegistry.convert(DataComponentValueImpl.class, key, dataComponentValue);
++ builder.set((DataComponentType) type, (Object) converted.value());
++ });
++ return builder.build();
++ }
++
++ public record DataComponentValueImpl<T>(com.mojang.serialization.Codec<T> codec, T value) implements DataComponentValue.TagSerializable {
++
++ @Override
++ public @NotNull BinaryTagHolder asBinaryTag() {
++ return BinaryTagHolder.encode(this.codec.encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE), this.value).getOrThrow(IllegalArgumentException::new), NBT_CODEC);
++ }
++ }
++
++ public static @Nullable BinaryTagHolder asBinaryTagHolder(final @Nullable CompoundTag tag) {
++ if (tag == null) {
++ return null;
++ }
++ return BinaryTagHolder.encode(tag, NBT_CODEC);
++ }
++
++ // Colors
++
++ public static @NotNull TextColor asAdventure(final ChatFormatting formatting) {
++ final Integer color = formatting.getColor();
++ if (color == null) {
++ throw new IllegalArgumentException("Not a valid color");
++ }
++ return TextColor.color(color);
++ }
++
++ public static @Nullable ChatFormatting asVanilla(final TextColor color) {
++ return ChatFormatting.getByHexValue(color.value());
++ }
++
++ // Style
++
++ public static net.minecraft.network.chat.Style asVanilla(final Style style) {
++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
++ final Object encoded = AdventureCodecs.STYLE_MAP_CODEC.codec()
++ .encodeStart(ops, style).getOrThrow(IllegalStateException::new);
++
++ return net.minecraft.network.chat.Style.Serializer.CODEC
++ .parse(ops, encoded).getOrThrow(IllegalStateException::new);
++ }
++
++ public static Style asAdventure(final net.minecraft.network.chat.Style style) {
++ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
++ final Object encoded = net.minecraft.network.chat.Style.Serializer.CODEC
++ .encodeStart(ops, style).getOrThrow(IllegalStateException::new);
++
++ return AdventureCodecs.STYLE_MAP_CODEC.codec()
++ .parse(ops, encoded).getOrThrow(IllegalStateException::new);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a16344476abbb4f3e8aac26d4add9da53b7fc7df
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java
+@@ -0,0 +1,43 @@
++package io.papermc.paper.adventure;
++
++import com.google.common.base.Suppliers;
++import com.mojang.datafixers.util.Pair;
++import com.mojang.serialization.JavaOps;
++import java.util.function.Supplier;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.serializer.ComponentSerializer;
++import net.minecraft.network.chat.ComponentSerialization;
++import net.minecraft.resources.RegistryOps;
++import org.bukkit.craftbukkit.CraftRegistry;
++
++public final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> {
++
++ private final Supplier<RegistryOps<Object>> javaOps;
++
++ public WrapperAwareSerializer(final Supplier<RegistryOps<Object>> javaOps) {
++ this.javaOps = Suppliers.memoize(javaOps::get);
++ }
++
++ @Override
++ public Component deserialize(final net.minecraft.network.chat.Component input) {
++ if (input instanceof AdventureComponent) {
++ return ((AdventureComponent) input).adventure;
++ }
++ final RegistryOps<Object> ops = this.javaOps.get();
++ final Object obj = ComponentSerialization.CODEC.encodeStart(ops, input)
++ .getOrThrow(s -> new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + s));
++ final Pair<Component, Object> converted = AdventureCodecs.COMPONENT_CODEC.decode(ops, obj)
++ .getOrThrow(s -> new RuntimeException("Failed to decode to adventure Component: " + obj + "; " + s));
++ return converted.getFirst();
++ }
++
++ @Override
++ public net.minecraft.network.chat.Component serialize(final Component component) {
++ final RegistryOps<Object> ops = this.javaOps.get();
++ final Object obj = AdventureCodecs.COMPONENT_CODEC.encodeStart(ops, component)
++ .getOrThrow(s -> new RuntimeException("Failed to encode adventure Component: " + component + "; " + s));
++ final Pair<net.minecraft.network.chat.Component, Object> converted = ComponentSerialization.CODEC.decode(ops, obj)
++ .getOrThrow(s -> new RuntimeException("Failed to decode to Minecraft Component: " + obj + "; " + s));
++ return converted.getFirst();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java b/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2ee72fe7cb56e70404b8c86f0c9578750a45af03
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/BossBarImplementationProvider.java
+@@ -0,0 +1,14 @@
++package io.papermc.paper.adventure.providers;
++
++import io.papermc.paper.adventure.BossBarImplementationImpl;
++import net.kyori.adventure.bossbar.BossBar;
++import net.kyori.adventure.bossbar.BossBarImplementation;
++import org.jetbrains.annotations.NotNull;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class BossBarImplementationProvider implements BossBarImplementation.Provider {
++ @Override
++ public @NotNull BossBarImplementation create(final @NotNull BossBar bar) {
++ return new BossBarImplementationImpl(bar);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..23432eea862c6df716d7726a32da3a0612a3fb77
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
+@@ -0,0 +1,96 @@
++package io.papermc.paper.adventure.providers;
++
++import java.util.HashMap;
++import java.util.Map;
++import java.util.UUID;
++import net.kyori.adventure.audience.Audience;
++import net.kyori.adventure.text.event.ClickCallback;
++import net.kyori.adventure.text.event.ClickEvent;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.Queue;
++import java.util.concurrent.ConcurrentLinkedQueue;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class ClickCallbackProviderImpl implements ClickCallback.Provider {
++
++ public static final CallbackManager CALLBACK_MANAGER = new CallbackManager();
++
++ @Override
++ public @NotNull ClickEvent create(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
++ return ClickEvent.runCommand("/paper:callback " + CALLBACK_MANAGER.addCallback(callback, options));
++ }
++
++ public static final class CallbackManager {
++
++ private final Map<UUID, StoredCallback> callbacks = new HashMap<>();
++ private final Queue<StoredCallback> queue = new ConcurrentLinkedQueue<>();
++
++ private CallbackManager() {
++ }
++
++ public UUID addCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
++ final UUID id = UUID.randomUUID();
++ this.queue.add(new StoredCallback(callback, options, id));
++ return id;
++ }
++
++ public void handleQueue(final int currentTick) {
++ // Evict expired entries
++ if (currentTick % 100 == 0) {
++ this.callbacks.values().removeIf(callback -> !callback.valid());
++ }
++
++ // Add entries from queue
++ StoredCallback callback;
++ while ((callback = this.queue.poll()) != null) {
++ this.callbacks.put(callback.id(), callback);
++ }
++ }
++
++ public void runCallback(final @NotNull Audience audience, final UUID id) {
++ final StoredCallback callback = this.callbacks.get(id);
++ if (callback != null && callback.valid()) { //TODO Message if expired/invalid?
++ callback.takeUse();
++ callback.callback.accept(audience);
++ }
++ }
++ }
++
++ private static final class StoredCallback {
++ private final long startedAt = System.nanoTime();
++ private final ClickCallback<Audience> callback;
++ private final long lifetime;
++ private final UUID id;
++ private int remainingUses;
++
++ private StoredCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options, final UUID id) {
++ this.callback = callback;
++ this.lifetime = options.lifetime().toNanos();
++ this.remainingUses = options.uses();
++ this.id = id;
++ }
++
++ public void takeUse() {
++ if (this.remainingUses != ClickCallback.UNLIMITED_USES) {
++ this.remainingUses--;
++ }
++ }
++
++ public boolean hasRemainingUses() {
++ return this.remainingUses == ClickCallback.UNLIMITED_USES || this.remainingUses > 0;
++ }
++
++ public boolean expired() {
++ return System.nanoTime() - this.startedAt >= this.lifetime;
++ }
++
++ public boolean valid() {
++ return hasRemainingUses() && !expired();
++ }
++
++ public UUID id() {
++ return this.id;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8323f135d6bf2e1f12525e05094ffa3f2420e7e1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.adventure.providers;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
++import org.jetbrains.annotations.NotNull;
++import org.slf4j.LoggerFactory;
++
++@SuppressWarnings("UnstableApiUsage")
++public class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
++ @Override
++ public @NotNull ComponentLogger logger(@NotNull LoggerHelper helper, @NotNull String name) {
++ return helper.delegating(LoggerFactory.getLogger(name), this::serialize);
++ }
++
++ private String serialize(final Component message) {
++ return PaperAdventure.asPlain(message, null);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ee2076fd098ae2164596f39b88f56b3700ed3687
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/DataComponentValueConverterProviderImpl.java
+@@ -0,0 +1,82 @@
++package io.papermc.paper.adventure.providers;
++
++import com.google.gson.JsonElement;
++import com.mojang.brigadier.exceptions.CommandSyntaxException;
++import com.mojang.serialization.DynamicOps;
++import com.mojang.serialization.JsonOps;
++import io.papermc.paper.adventure.PaperAdventure;
++import java.util.List;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.nbt.api.BinaryTagHolder;
++import net.kyori.adventure.text.event.DataComponentValue;
++import net.kyori.adventure.text.event.DataComponentValueConverterRegistry;
++import net.kyori.adventure.text.serializer.gson.GsonDataComponentValue;
++import net.minecraft.core.component.DataComponentType;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.resources.RegistryOps;
++import org.bukkit.craftbukkit.CraftRegistry;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import static net.kyori.adventure.text.serializer.gson.GsonDataComponentValue.gsonDataComponentValue;
++
++@DefaultQualifier(NonNull.class)
++public class DataComponentValueConverterProviderImpl implements DataComponentValueConverterRegistry.Provider {
++
++ static final Key ID = Key.key("adventure", "platform/paper");
++
++ @Override
++ public Key id() {
++ return ID;
++ }
++
++ private static <T> RegistryOps<T> createOps(final DynamicOps<T> delegate) {
++ return CraftRegistry.getMinecraftRegistry().createSerializationContext(delegate);
++ }
++
++ @SuppressWarnings({"unchecked", "rawtypes"})
++ @Override
++ public Iterable<DataComponentValueConverterRegistry.Conversion<?, ?>> conversions() {
++ return List.of(
++ DataComponentValueConverterRegistry.Conversion.convert(
++ PaperAdventure.DataComponentValueImpl.class,
++ GsonDataComponentValue.class,
++ (key, dataComponentValue) -> gsonDataComponentValue((JsonElement) dataComponentValue.codec().encodeStart(createOps(JsonOps.INSTANCE), dataComponentValue.value()).getOrThrow())
++ ),
++ DataComponentValueConverterRegistry.Conversion.convert(
++ GsonDataComponentValue.class,
++ PaperAdventure.DataComponentValueImpl.class,
++ (key, dataComponentValue) -> {
++ final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(PaperAdventure.asVanilla(key));
++ if (type == null) {
++ throw new IllegalArgumentException("Unknown data component type: " + key);
++ }
++ return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(JsonOps.INSTANCE), dataComponentValue.element()).getOrThrow(IllegalArgumentException::new));
++ }
++ ),
++ DataComponentValueConverterRegistry.Conversion.convert(
++ PaperAdventure.DataComponentValueImpl.class,
++ DataComponentValue.TagSerializable.class,
++ (key, dataComponentValue) -> BinaryTagHolder.encode((Tag) dataComponentValue.codec().encodeStart(createOps(NbtOps.INSTANCE), dataComponentValue.value()).getOrThrow(), PaperAdventure.NBT_CODEC)
++ ),
++ DataComponentValueConverterRegistry.Conversion.convert(
++ DataComponentValue.TagSerializable.class,
++ PaperAdventure.DataComponentValueImpl.class,
++ (key, tagSerializable) -> {
++ final @Nullable DataComponentType<?> type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(PaperAdventure.asVanilla(key));
++ if (type == null) {
++ throw new IllegalArgumentException("Unknown data component type: " + key);
++ }
++ try {
++ return new PaperAdventure.DataComponentValueImpl(type.codecOrThrow(), type.codecOrThrow().parse(createOps(NbtOps.INSTANCE), tagSerializable.asBinaryTag().get(PaperAdventure.NBT_CODEC)).getOrThrow(IllegalArgumentException::new));
++ } catch (final CommandSyntaxException e) {
++ throw new IllegalArgumentException(e);
++ }
++ }
++ )
++ );
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c620d5aa2b0208b769dbe9563f0e99edc9a91047
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/GsonComponentSerializerProviderImpl.java
+@@ -0,0 +1,30 @@
++package io.papermc.paper.adventure.providers;
++
++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.function.Consumer;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class GsonComponentSerializerProviderImpl implements GsonComponentSerializer.Provider {
++
++ @Override
++ public @NotNull GsonComponentSerializer gson() {
++ return GsonComponentSerializer.builder()
++ .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE)
++ .build();
++ }
++
++ @Override
++ public @NotNull GsonComponentSerializer gsonLegacy() {
++ return GsonComponentSerializer.builder()
++ .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE)
++ .downsampleColors()
++ .build();
++ }
++
++ @Override
++ public @NotNull Consumer<GsonComponentSerializer.Builder> builder() {
++ return builder -> builder.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.INSTANCE);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..03723dbe32b7eb95253e8ff6e72dbf8d2300a059
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/LegacyComponentSerializerProviderImpl.java
+@@ -0,0 +1,36 @@
++package io.papermc.paper.adventure.providers;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.function.Consumer;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class LegacyComponentSerializerProviderImpl implements LegacyComponentSerializer.Provider {
++
++ @Override
++ public @NotNull LegacyComponentSerializer legacyAmpersand() {
++ return LegacyComponentSerializer.builder()
++ .flattener(PaperAdventure.FLATTENER)
++ .character(LegacyComponentSerializer.AMPERSAND_CHAR)
++ .hexColors()
++ .useUnusualXRepeatedCharacterHexFormat()
++ .build();
++ }
++
++ @Override
++ public @NotNull LegacyComponentSerializer legacySection() {
++ return LegacyComponentSerializer.builder()
++ .flattener(PaperAdventure.FLATTENER)
++ .character(LegacyComponentSerializer.SECTION_CHAR)
++ .hexColors()
++ .useUnusualXRepeatedCharacterHexFormat()
++ .build();
++ }
++
++ @Override
++ public @NotNull Consumer<LegacyComponentSerializer.Builder> legacy() {
++ return builder -> builder.flattener(PaperAdventure.FLATTENER);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..25fd6992c869c841b1b1b3240f4d524948487614
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/MiniMessageProviderImpl.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.adventure.providers;
++
++import net.kyori.adventure.text.minimessage.MiniMessage;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.function.Consumer;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class MiniMessageProviderImpl implements MiniMessage.Provider {
++
++ @Override
++ public @NotNull MiniMessage miniMessage() {
++ return MiniMessage.builder().build();
++ }
++
++ @Override
++ public @NotNull Consumer<MiniMessage.Builder> builder() {
++ return builder -> {};
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java b/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..202b964e7e67717904cd3f00b6af6ad7f2a5c90e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/NBTLegacyHoverEventSerializer.java
+@@ -0,0 +1,91 @@
++package io.papermc.paper.adventure.providers;
++
++import com.mojang.brigadier.exceptions.CommandSyntaxException;
++import java.io.IOException;
++import java.util.UUID;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.nbt.api.BinaryTagHolder;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.event.HoverEvent;
++import net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import net.kyori.adventure.util.Codec;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.nbt.TagParser;
++import org.intellij.lang.annotations.Subst;
++
++final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer {
++ public static final NBTLegacyHoverEventSerializer INSTANCE = new NBTLegacyHoverEventSerializer();
++ private static final Codec<CompoundTag, String, CommandSyntaxException, RuntimeException> SNBT_CODEC = Codec.codec(TagParser::parseTag, Tag::toString);
++
++ static final String ITEM_TYPE = "id";
++ static final String ITEM_COUNT = "Count";
++ static final String ITEM_TAG = "tag";
++
++ static final String ENTITY_NAME = "name";
++ static final String ENTITY_TYPE = "type";
++ static final String ENTITY_ID = "id";
++
++ NBTLegacyHoverEventSerializer() {
++ }
++
++ @Override
++ public HoverEvent.ShowItem deserializeShowItem(final Component input) throws IOException {
++ final String raw = PlainTextComponentSerializer.plainText().serialize(input);
++ try {
++ final CompoundTag contents = SNBT_CODEC.decode(raw);
++ final CompoundTag tag = contents.getCompound(ITEM_TAG);
++ @Subst("key") final String keyString = contents.getString(ITEM_TYPE);
++ return HoverEvent.ShowItem.showItem(
++ Key.key(keyString),
++ contents.contains(ITEM_COUNT) ? contents.getByte(ITEM_COUNT) : 1,
++ tag.isEmpty() ? null : BinaryTagHolder.encode(tag, SNBT_CODEC)
++ );
++ } catch (final CommandSyntaxException ex) {
++ throw new IOException(ex);
++ }
++ }
++
++ @Override
++ public HoverEvent.ShowEntity deserializeShowEntity(final Component input, final Codec.Decoder<Component, String, ? extends RuntimeException> componentCodec) throws IOException {
++ final String raw = PlainTextComponentSerializer.plainText().serialize(input);
++ try {
++ final CompoundTag contents = SNBT_CODEC.decode(raw);
++ @Subst("key") final String keyString = contents.getString(ENTITY_TYPE);
++ return HoverEvent.ShowEntity.showEntity(
++ Key.key(keyString),
++ UUID.fromString(contents.getString(ENTITY_ID)),
++ componentCodec.decode(contents.getString(ENTITY_NAME))
++ );
++ } catch (final CommandSyntaxException ex) {
++ throw new IOException(ex);
++ }
++ }
++
++ @Override
++ public Component serializeShowItem(final HoverEvent.ShowItem input) throws IOException {
++ final CompoundTag tag = new CompoundTag();
++ tag.putString(ITEM_TYPE, input.item().asString());
++ tag.putByte(ITEM_COUNT, (byte) input.count());
++ if (input.nbt() != null) {
++ try {
++ tag.put(ITEM_TAG, input.nbt().get(SNBT_CODEC));
++ } catch (final CommandSyntaxException ex) {
++ throw new IOException(ex);
++ }
++ }
++ return Component.text(SNBT_CODEC.encode(tag));
++ }
++
++ @Override
++ public Component serializeShowEntity(final HoverEvent.ShowEntity input, final Codec.Encoder<Component, String, ? extends RuntimeException> componentCodec) {
++ final CompoundTag tag = new CompoundTag();
++ tag.putString(ENTITY_ID, input.id().toString());
++ tag.putString(ENTITY_TYPE, input.type().asString());
++ if (input.name() != null) {
++ tag.putString(ENTITY_NAME, componentCodec.encode(input.name()));
++ }
++ return Component.text(SNBT_CODEC.encode(tag));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c0701d4f93a4d77a8177d2dd8d5076f9f781873d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/providers/PlainTextComponentSerializerProviderImpl.java
+@@ -0,0 +1,23 @@
++package io.papermc.paper.adventure.providers;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.function.Consumer;
++
++@SuppressWarnings("UnstableApiUsage") // permitted provider
++public class PlainTextComponentSerializerProviderImpl implements PlainTextComponentSerializer.Provider {
++
++ @Override
++ public @NotNull PlainTextComponentSerializer plainTextSimple() {
++ return PlainTextComponentSerializer.builder()
++ .flattener(PaperAdventure.FLATTENER)
++ .build();
++ }
++
++ @Override
++ public @NotNull Consumer<PlainTextComponentSerializer.Builder> plainText() {
++ return builder -> builder.flattener(PaperAdventure.FLATTENER);
++ }
++}
+diff --git a/src/main/java/net/minecraft/ChatFormatting.java b/src/main/java/net/minecraft/ChatFormatting.java
+index 08dcd817bfe1ba0121d4ce701825e4aee384db85..d5f63d06d921d731b4e64b38228377712fe9c75b 100644
+--- a/src/main/java/net/minecraft/ChatFormatting.java
++++ b/src/main/java/net/minecraft/ChatFormatting.java
+@@ -112,6 +112,18 @@ public enum ChatFormatting implements StringRepresentable {
+ return name == null ? null : FORMATTING_BY_NAME.get(cleanName(name));
+ }
+
++ // Paper start - add method to get by hex value
++ @Nullable public static ChatFormatting getByHexValue(int i) {
++ for (ChatFormatting value : values()) {
++ if (value.getColor() != null && value.getColor() == i) {
++ return value;
++ }
++ }
++
++ return null;
++ }
++ // Paper end - add method to get by hex value
++
+ @Nullable
+ public static ChatFormatting getById(int colorIndex) {
+ if (colorIndex < 0) {
+diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java
+index d6ea21d6d33b701f249a8acd3e7304eb2c02e1ca..2fbd7f7c976fb55b7238f1e512afad79e52a5b2c 100644
+--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java
++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java
+@@ -67,6 +67,7 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS
+ private final CommandSigningContext signingContext;
+ private final TaskChainer chatMessageChainer;
+ public volatile CommandNode currentCommand; // CraftBukkit
++ public boolean bypassSelectorPermissions = false; // Paper - add bypass for selector permissions
+
+ public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) {
+ this(output, pos, rot, world, level, name, displayName, server, entity, false, CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server));
+diff --git a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
+index 8f03eed020a99b96189f4f5d42d806d06f4d6b5e..55484826fc5ddd04ae024e25a0251796d7fa9c28 100644
+--- a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
++++ b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
+@@ -54,17 +54,21 @@ public class MessageArgument implements SignedArgument<MessageArgument.Message>
+ private static void resolveSignedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) {
+ MinecraftServer minecraftServer = source.getServer();
+ CompletableFuture<FilteredText> completableFuture = filterPlainText(source, message);
+- Component component = minecraftServer.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent());
+- source.getChatMessageChainer().append(completableFuture, filtered -> {
+- PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask());
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture<Component> componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> {
++ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
++ // Paper end - support asynchronous chat decoration
+ callback.accept(playerChatMessage2);
+ });
+ }
+
+ private static void resolveDisguisedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) {
+ ChatDecorator chatDecorator = source.getServer().getChatDecorator();
+- Component component = chatDecorator.decorate(source.getPlayer(), message.decoratedContent());
+- callback.accept(message.withUnsignedContent(component));
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture<Component> componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result)));
++ // Paper end - support asynchronous chat decoration
+ }
+
+ private static CompletableFuture<FilteredText> filterPlainText(CommandSourceStack source, PlayerChatMessage message) {
+diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
+index dd66979bcae33096d72001678e8e55569bea6f53..c8d39e6e1c570c9219f6066da273dc0130920519 100644
+--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
+@@ -93,7 +93,7 @@ public class EntitySelector {
+ }
+
+ private void checkPermissions(CommandSourceStack source) throws CommandSyntaxException {
+- if (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector")) { // CraftBukkit
++ if (!source.bypassSelectorPermissions && (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector"))) { // CraftBukkit // Paper - add bypass for selector perms
+ throw EntityArgument.ERROR_SELECTORS_NOT_ALLOWED.create();
+ }
+ }
+diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java
+index 7bf8cb128a6bad9c5a3405f4137b837638ce3f50..a523a83aec3a6ecbec4d60a187edc0c0167d15b4 100644
+--- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java
++++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java
+@@ -72,6 +72,7 @@ public class FriendlyByteBuf extends ByteBuf {
+
+ public static final int DEFAULT_NBT_QUOTA = 2097152;
+ private final ByteBuf source;
++ @Nullable public final java.util.Locale adventure$locale; // Paper - track player's locale for server-side translations
+ public static final short MAX_STRING_LENGTH = Short.MAX_VALUE;
+ public static final int MAX_COMPONENT_STRING_LENGTH = 262144;
+ private static final int PUBLIC_KEY_SIZE = 256;
+@@ -80,6 +81,7 @@ public class FriendlyByteBuf extends ByteBuf {
+ private static final Gson GSON = new Gson();
+
+ public FriendlyByteBuf(ByteBuf parent) {
++ this.adventure$locale = PacketEncoder.ADVENTURE_LOCALE.get(); // Paper - track player's locale for server-side translations
+ this.source = parent;
+ }
+
+@@ -120,11 +122,16 @@ public class FriendlyByteBuf extends ByteBuf {
+ }
+
+ public <T> void writeJsonWithCodec(Codec<T> codec, T value) {
++ // Paper start - Adventure; add max length parameter
++ this.writeJsonWithCodec(codec, value, MAX_STRING_LENGTH);
++ }
++ public <T> void writeJsonWithCodec(Codec<T> codec, T value, int maxLength) {
++ // Paper end - Adventure; add max length parameter
+ DataResult<JsonElement> dataresult = codec.encodeStart(JsonOps.INSTANCE, value);
+
+ this.writeUtf(FriendlyByteBuf.GSON.toJson((JsonElement) dataresult.getOrThrow((s) -> {
+ return new EncoderException("Failed to encode: " + s + " " + String.valueOf(value));
+- })));
++ })), maxLength); // Paper - Adventure; add max length parameter
+ }
+
+ public static <T> IntFunction<T> limitValue(IntFunction<T> applier, int max) {
+diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java
+index a58f67973b4ed986065860263c7a42214640520d..046bfc212b640de174b300e7a05cc30bb3cac93e 100644
+--- a/src/main/java/net/minecraft/network/PacketEncoder.java
++++ b/src/main/java/net/minecraft/network/PacketEncoder.java
+@@ -17,10 +17,12 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode
+ this.protocolInfo = state;
+ }
+
++ static final ThreadLocal<java.util.Locale> ADVENTURE_LOCALE = ThreadLocal.withInitial(() -> null); // Paper - adventure; set player's locale
+ protected void encode(ChannelHandlerContext channelHandlerContext, Packet<T> packet, ByteBuf byteBuf) throws Exception {
+ PacketType<? extends Packet<? super T>> packetType = packet.type();
+
+ try {
++ ADVENTURE_LOCALE.set(channelHandlerContext.channel().attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).get()); // Paper - adventure; set player's locale
+ this.protocolInfo.codec().encode(byteBuf, packet);
+ int i = byteBuf.readableBytes();
+ if (LOGGER.isDebugEnabled()) {
+diff --git a/src/main/java/net/minecraft/network/chat/ChatDecorator.java b/src/main/java/net/minecraft/network/chat/ChatDecorator.java
+index e4624d696dcf0ddb6d42a80701dfc47ec6877540..6b8dc1eb490098cc14673c9ab0aa16fe3471325b 100644
+--- a/src/main/java/net/minecraft/network/chat/ChatDecorator.java
++++ b/src/main/java/net/minecraft/network/chat/ChatDecorator.java
+@@ -2,10 +2,18 @@ package net.minecraft.network.chat;
+
+ import javax.annotation.Nullable;
+ import net.minecraft.server.level.ServerPlayer;
++import java.util.concurrent.CompletableFuture; // Paper
+
+ @FunctionalInterface
+ public interface ChatDecorator {
+- ChatDecorator PLAIN = (sender, message) -> message;
++ ChatDecorator PLAIN = (sender, message) -> CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events
+
+- Component decorate(@Nullable ServerPlayer sender, Component message);
++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events (callers should use the overload with CommandSourceStack)
++ CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, Component message); // Paper - adventure; support async chat decoration events
++
++ // Paper start - adventure; support async chat decoration events
++ default CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
++ throw new UnsupportedOperationException("Must override this implementation");
++ }
++ // Paper end - adventure; support async chat decoration events
+ }
+diff --git a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java
+index ec99dc00d83e2369e66ba5e93bf482555aab7b06..22c6a324dedeb315eac2d3d3f55d2f3a9eebb0ad 100644
+--- a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java
++++ b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java
+@@ -37,9 +37,31 @@ import net.minecraft.util.StringRepresentable;
+
+ public class ComponentSerialization {
+ public static final Codec<Component> CODEC = Codec.recursive("Component", ComponentSerialization::createCodec);
+- public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(CODEC);
++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = createTranslationAware(() -> net.minecraft.nbt.NbtAccounter.create(net.minecraft.network.FriendlyByteBuf.DEFAULT_NBT_QUOTA)); // Paper - adventure
+ public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> OPTIONAL_STREAM_CODEC = STREAM_CODEC.apply(ByteBufCodecs::optional);
+- public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(CODEC);
++ // Paper start - adventure; use locale from bytebuf for translation
++ public static final ThreadLocal<Boolean> DONT_RENDER_TRANSLATABLES = ThreadLocal.withInitial(() -> false);
++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = createTranslationAware(net.minecraft.nbt.NbtAccounter::unlimitedHeap);
++ private static StreamCodec<RegistryFriendlyByteBuf, Component> createTranslationAware(final Supplier<net.minecraft.nbt.NbtAccounter> sizeTracker) {
++ return new StreamCodec<>() {
++ final StreamCodec<ByteBuf, net.minecraft.nbt.Tag> streamCodec = ByteBufCodecs.tagCodec(sizeTracker);
++ @Override
++ public Component decode(RegistryFriendlyByteBuf registryFriendlyByteBuf) {
++ net.minecraft.nbt.Tag tag = this.streamCodec.decode(registryFriendlyByteBuf);
++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE);
++ return CODEC.parse(registryOps, tag).getOrThrow(error -> new io.netty.handler.codec.DecoderException("Failed to decode: " + error + " " + tag));
++ }
++
++ @Override
++ public void encode(RegistryFriendlyByteBuf registryFriendlyByteBuf, Component object) {
++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE);
++ net.minecraft.nbt.Tag tag = (DONT_RENDER_TRANSLATABLES.get() ? CODEC : ComponentSerialization.localizedCodec(registryFriendlyByteBuf.adventure$locale))
++ .encodeStart(registryOps, object).getOrThrow(error -> new io.netty.handler.codec.EncoderException("Failed to encode: " + error + " " + object));
++ this.streamCodec.encode(registryFriendlyByteBuf, tag);
++ }
++ };
++ }
++ // Paper end - adventure; use locale from bytebuf for translation
+ public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> TRUSTED_OPTIONAL_STREAM_CODEC = TRUSTED_STREAM_CODEC.apply(
+ ByteBufCodecs::optional
+ );
+@@ -100,7 +122,27 @@ public class ComponentSerialization {
+ return ExtraCodecs.orCompressed(mapCodec3, mapCodec2);
+ }
+
++ // Paper start - adventure; create separate codec for each locale
++ private static final java.util.Map<java.util.Locale, Codec<Component>> LOCALIZED_CODECS = new java.util.concurrent.ConcurrentHashMap<>();
++
++ public static Codec<Component> localizedCodec(final [email protected] Locale locale) {
++ if (locale == null) {
++ return CODEC;
++ }
++ return LOCALIZED_CODECS.computeIfAbsent(locale,
++ loc -> Codec.recursive("Component", selfCodec -> createCodec(selfCodec, loc)));
++ }
++
++
++ // Paper end - adventure; create separate codec for each locale
++
+ private static Codec<Component> createCodec(Codec<Component> selfCodec) {
++ // Paper start - adventure; create separate codec for each locale
++ return createCodec(selfCodec, null);
++ }
++
++ private static Codec<Component> createCodec(Codec<Component> selfCodec, @javax.annotation.Nullable java.util.Locale locale) {
++ // Paper end - adventure; create separate codec for each locale
+ ComponentContents.Type<?>[] types = new ComponentContents.Type[]{
+ PlainTextContents.TYPE, TranslatableContents.TYPE, KeybindContents.TYPE, ScoreContents.TYPE, SelectorContents.TYPE, NbtContents.TYPE
+ };
+@@ -113,6 +155,34 @@ public class ComponentSerialization {
+ )
+ .apply(instance, MutableComponent::new)
+ );
++ // Paper start - adventure; create separate codec for each locale
++ final Codec<Component> origCodec = codec;
++ codec = new Codec<>() {
++ @Override
++ public <T> DataResult<com.mojang.datafixers.util.Pair<Component, T>> decode(final DynamicOps<T> ops, final T input) {
++ return origCodec.decode(ops, input);
++ }
++
++ @Override
++ public <T> DataResult<T> encode(final Component input, final DynamicOps<T> ops, final T prefix) {
++ final net.kyori.adventure.text.Component adventureComponent;
++ if (input instanceof io.papermc.paper.adventure.AdventureComponent adv) {
++ adventureComponent = adv.adventure$component();
++ } else if (locale != null && input.getContents() instanceof TranslatableContents && io.papermc.paper.adventure.PaperAdventure.hasAnyTranslations()) {
++ adventureComponent = io.papermc.paper.adventure.PaperAdventure.asAdventure(input);
++ } else {
++ return origCodec.encode(input, ops, prefix);
++ }
++ return io.papermc.paper.adventure.PaperAdventure.localizedCodec(locale)
++ .encode(adventureComponent, ops, prefix);
++ }
++
++ @Override
++ public String toString() {
++ return origCodec.toString() + "[AdventureComponentAware]";
++ }
++ };
++ // Paper end - adventure; create separate codec for each locale
+ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), codec)
+ .xmap(either -> either.map(either2 -> either2.map(Component::literal, ComponentSerialization::createFromList), text -> (Component)text), text -> {
+ String string = text.tryCollapseToString();
+diff --git a/src/main/java/net/minecraft/network/chat/ComponentUtils.java b/src/main/java/net/minecraft/network/chat/ComponentUtils.java
+index 0030c0c91e989fcdc5b7ce6490836a0e8dd3b5d5..3365aed2b67ae0e4dd0410f5190ba474f146139b 100644
+--- a/src/main/java/net/minecraft/network/chat/ComponentUtils.java
++++ b/src/main/java/net/minecraft/network/chat/ComponentUtils.java
+@@ -41,6 +41,11 @@ public class ComponentUtils {
+ if (depth > 100) {
+ return text.copy();
+ } else {
++ // Paper start - adventure; pass actual vanilla component
++ if (text instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++ text = adventureComponent.deepConverted();
++ }
++ // Paper end - adventure; pass actual vanilla component
+ MutableComponent mutableComponent = text.getContents().resolve(source, sender, depth + 1);
+
+ for (Component component : text.getSiblings()) {
+diff --git a/src/main/java/net/minecraft/network/chat/MessageSignature.java b/src/main/java/net/minecraft/network/chat/MessageSignature.java
+index 739ef5fb6f4fa37382153ba6a308ca3b451e6b05..7c3154af5d7732037c0ee965f6f8b89424461abd 100644
+--- a/src/main/java/net/minecraft/network/chat/MessageSignature.java
++++ b/src/main/java/net/minecraft/network/chat/MessageSignature.java
+@@ -13,6 +13,7 @@ import net.minecraft.util.SignatureUpdater;
+ import net.minecraft.util.SignatureValidator;
+
+ public record MessageSignature(byte[] bytes) {
++ public net.kyori.adventure.chat.SignedMessage.Signature adventure() { return () -> this.bytes; } // Paper - adventure; support signed messages
+ public static final Codec<MessageSignature> CODEC = ExtraCodecs.BASE64_STRING.xmap(MessageSignature::new, MessageSignature::bytes);
+ public static final int BYTES = 256;
+
+diff --git a/src/main/java/net/minecraft/network/chat/MutableComponent.java b/src/main/java/net/minecraft/network/chat/MutableComponent.java
+index 0a70fb7df0d4532edbc2468b13520c34ae1500e9..e34a8a66411b7571813117ce47d9dec08e567978 100644
+--- a/src/main/java/net/minecraft/network/chat/MutableComponent.java
++++ b/src/main/java/net/minecraft/network/chat/MutableComponent.java
+@@ -94,6 +94,11 @@ public class MutableComponent implements Component {
+
+ @Override
+ public boolean equals(Object object) {
++ // Paper start - make AdventureComponent equivalent
++ if (object instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++ object = adventureComponent.deepConverted();
++ }
++ // Paper end - make AdventureComponent equivalent
+ return this == object
+ || object instanceof MutableComponent mutableComponent
+ && this.contents.equals(mutableComponent.contents)
+diff --git a/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java b/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java
+index e2def0ca552343143e495736d533b3334686fd62..c87b708c368713a23a10ad97704575ee4df27891 100644
+--- a/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java
++++ b/src/main/java/net/minecraft/network/chat/OutgoingChatMessage.java
+@@ -7,6 +7,12 @@ public interface OutgoingChatMessage {
+
+ void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params);
+
++ // Paper start
++ default void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
++ this.sendToPlayer(sender, filterMaskEnabled, params);
++ }
++ // Paper end
++
+ static OutgoingChatMessage create(PlayerChatMessage message) {
+ return (OutgoingChatMessage)(message.isSystem()
+ ? new OutgoingChatMessage.Disguised(message.decoratedContent())
+@@ -16,7 +22,12 @@ public interface OutgoingChatMessage {
+ public static record Disguised(@Override Component content) implements OutgoingChatMessage {
+ @Override
+ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) {
+- sender.connection.sendDisguisedChatMessage(this.content, params);
++ // Paper start
++ this.sendToPlayer(sender, filterMaskEnabled, params, null);
++ }
++ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
++ sender.connection.sendDisguisedChatMessage(unsigned != null ? unsigned : this.content, params);
++ // Paper end
+ }
+ }
+
+@@ -28,7 +39,13 @@ public interface OutgoingChatMessage {
+
+ @Override
+ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) {
++ // Paper start
++ this.sendToPlayer(sender, filterMaskEnabled, params, null);
++ }
++ public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
++ // Paper end
+ PlayerChatMessage playerChatMessage = this.message.filter(filterMaskEnabled);
++ playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper
+ if (!playerChatMessage.isFullyFiltered()) {
+ sender.connection.sendPlayerChatMessage(playerChatMessage, params);
+ }
+diff --git a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java b/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java
+index d89049328641faa889b7c567123ab3a2c63b8df0..76b304560c2b631a18f20e656a65ac75af7b4e63 100644
+--- a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java
++++ b/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java
+@@ -17,6 +17,42 @@ import net.minecraft.util.SignatureValidator;
+ public record PlayerChatMessage(
+ SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask
+ ) {
++ // Paper start - adventure; support signed messages
++ public final class AdventureView implements net.kyori.adventure.chat.SignedMessage {
++ private AdventureView() {
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull Instant timestamp() {
++ return PlayerChatMessage.this.timeStamp();
++ }
++ @Override
++ public long salt() {
++ return PlayerChatMessage.this.salt();
++ }
++ @Override
++ public @org.jetbrains.annotations.Nullable Signature signature() {
++ return PlayerChatMessage.this.signature == null ? null : PlayerChatMessage.this.signature.adventure();
++ }
++ @Override
++ public [email protected] Component unsignedContent() {
++ return PlayerChatMessage.this.unsignedContent() == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(PlayerChatMessage.this.unsignedContent());
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull String message() {
++ return PlayerChatMessage.this.signedContent();
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull net.kyori.adventure.identity.Identity identity() {
++ return net.kyori.adventure.identity.Identity.identity(PlayerChatMessage.this.sender());
++ }
++ public PlayerChatMessage playerChatMessage() {
++ return PlayerChatMessage.this;
++ }
++ }
++ public AdventureView adventureView() {
++ return new AdventureView();
++ }
++ // Paper end - adventure; support signed messages
+ public static final MapCodec<PlayerChatMessage> MAP_CODEC = RecordCodecBuilder.mapCodec(
+ instance -> instance.group(
+ SignedMessageLink.CODEC.fieldOf("link").forGetter(PlayerChatMessage::link),
+@@ -47,7 +83,14 @@ public record PlayerChatMessage(
+ }
+
+ public PlayerChatMessage withUnsignedContent(Component unsignedContent) {
+- Component component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++ // Paper start - adventure
++ final Component component;
++ if (unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent) {
++ component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : unsignedContent;
++ } else {
++ component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++ }
++ // Paper end - adventure
+ return new PlayerChatMessage(this.link, this.signature, this.signedBody, component, this.filterMask);
+ }
+
+diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java
+index c5ba5e30cd74ca2cfc3f952c7b992df239a53a34..fdb75db02603ef97b624219211a0db22cd447dea 100644
+--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java
+@@ -18,6 +18,11 @@ public record ClientboundSystemChatPacket(Component content, boolean overlay) im
+ this(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(content)), overlay);
+ }
+ // Spigot end
++ // Paper start
++ public ClientboundSystemChatPacket(net.kyori.adventure.text.Component content, boolean overlay) {
++ this(io.papermc.paper.adventure.PaperAdventure.asVanilla(content), overlay);
++ }
++ // Paper end
+
+ @Override
+ public PacketType<ClientboundSystemChatPacket> type() {
+diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
+index bb97fdb9aa6167083442a928276ebe4225a586ef..eeaa40e8121643c6c1d951e76e7361e29210ba48 100644
+--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
+@@ -18,11 +18,16 @@ public class ClientboundLoginDisconnectPacket implements Packet<ClientLoginPacke
+ }
+
+ private ClientboundLoginDisconnectPacket(FriendlyByteBuf buf) {
+- this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(262144), RegistryAccess.EMPTY);
++ this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH), RegistryAccess.EMPTY); // Paper - diff on change
+ }
+
+ private void write(FriendlyByteBuf buf) {
+- buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
++ // Paper start - Adventure
++ // buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
++ // In the login phase, buf.adventure$locale field is most likely null, but plugins may use internals to set it via the channel attribute
++ java.util.Locale bufLocale = buf.adventure$locale;
++ buf.writeJsonWithCodec(net.minecraft.network.chat.ComponentSerialization.localizedCodec(bufLocale == null ? java.util.Locale.US : bufLocale), this.reason, FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH);
++ // Paper end - Adventure
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index cbdc5f9c54f24ae09881b3c8dfe980f79d32ee4b..8c68969b7d22376cfe5aadf81a16d9ba45e7c131 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -208,6 +208,7 @@ import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
+
+ public static final Logger LOGGER = LogUtils.getLogger();
++ public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper
+ public static final String VANILLA_BRAND = "vanilla";
+ private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F;
+ private static final int TICK_STATS_SPAN = 100;
+@@ -257,8 +258,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ private boolean preventProxyConnections;
+ private boolean pvp;
+ private boolean allowFlight;
+- @Nullable
+- private String motd;
++ private net.kyori.adventure.text.Component motd; // Paper - Adventure
+ private int playerIdleTimeout;
+ private final long[] tickTimesNanos;
+ private long aggregatedTickTimesNanos;
+@@ -1470,7 +1470,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ private ServerStatus buildServerStatus() {
+ ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus();
+
+- return new ServerStatus(Component.nullToEmpty(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile());
++ return new ServerStatus(io.papermc.paper.adventure.PaperAdventure.asVanilla(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile()); // Paper - Adventure
+ }
+
+ private ServerStatus.Players buildPlayerStatus() {
+@@ -1504,6 +1504,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ SpigotTimings.schedulerTimer.startTiming(); // Spigot
+ this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
+ SpigotTimings.schedulerTimer.stopTiming(); // Spigot
++ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
+ gameprofilerfiller.push("commandFunctions");
+ SpigotTimings.commandFunctionsTimer.startTiming(); // Spigot
+ this.getFunctions().tick();
+@@ -1877,10 +1878,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ @Override
+ public String getMotd() {
+- return this.motd;
++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.motd); // Paper - Adventure
+ }
+
+ public void setMotd(String motd) {
++ // Paper start - Adventure
++ this.motd = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserializeOr(motd, net.kyori.adventure.text.Component.empty());
++ }
++
++ public net.kyori.adventure.text.Component motd() {
++ return this.motd;
++ }
++
++ public void motd(net.kyori.adventure.text.Component motd) {
++ // Paper end - Adventure
+ this.motd = motd;
+ }
+
+@@ -2638,23 +2649,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+
+ public void logChatMessage(Component message, ChatType.Bound params, @Nullable String prefix) {
+- String s1 = params.decorate(message).getString();
++ // Paper start
++ net.kyori.adventure.text.Component s1 = io.papermc.paper.adventure.PaperAdventure.asAdventure(params.decorate(message));
+
+ if (prefix != null) {
+- MinecraftServer.LOGGER.info("[{}] {}", prefix, s1);
++ MinecraftServer.COMPONENT_LOGGER.info("[{}] {}", prefix, s1);
+ } else {
+- MinecraftServer.LOGGER.info("{}", s1);
++ MinecraftServer.COMPONENT_LOGGER.info("{}", s1);
++ // Paper end
+ }
+
+ }
+
+- // CraftBukkit start
+ public final java.util.concurrent.ExecutorService chatExecutor = java.util.concurrent.Executors.newCachedThreadPool(
+- new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").build());
+- // CraftBukkit end
++ new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); // Paper
+
++ public final ChatDecorator improvedChatDecorator = new io.papermc.paper.adventure.ImprovedChatDecorator(this); // Paper - adventure
+ public ChatDecorator getChatDecorator() {
+- return ChatDecorator.PLAIN;
++ return this.improvedChatDecorator; // Paper - support async chat decoration events
+ }
+
+ public boolean logIPs() {
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 50c255c5226d50f78ead4c0a0694ac9d2df490f3..3228c664925b1214aae0a693b93fdbe4d6698aa0 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -189,6 +189,7 @@ import net.minecraft.world.item.trading.MerchantOffers;
+ import net.minecraft.world.scores.Scoreboard;
+ import net.minecraft.world.scores.Team;
+ import net.minecraft.world.scores.criteria.ObjectiveCriteria;
++import io.papermc.paper.adventure.PaperAdventure; // Paper
+ import org.bukkit.Bukkit;
+ import org.bukkit.Location;
+ import org.bukkit.WeatherType;
+@@ -259,6 +260,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ private boolean disconnected;
+ private int requestedViewDistance;
+ public String language = "en_us"; // CraftBukkit - default
++ public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
+ @Nullable
+ private Vec3 startingToFallPosition;
+ @Nullable
+@@ -295,6 +297,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ // CraftBukkit start
+ public CraftPlayer.TransferCookieConnection transferCookieConnection;
+ public String displayName;
++ public net.kyori.adventure.text.Component adventure$displayName; // Paper
+ public Component listName;
+ public int listOrder = 0;
+ public org.bukkit.Location compassTarget;
+@@ -412,6 +415,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+
+ // CraftBukkit start
+ this.displayName = this.getScoreboardName();
++ this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper
+ this.bukkitPickUpLoot = true;
+ this.maxHealthCache = this.getMaxHealth();
+ }
+@@ -1168,22 +1172,17 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+
+ String deathmessage = defaultMessage.getString();
+ this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel
+- org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, deathmessage, keepInventory);
++ org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure
+
+ // SPIGOT-943 - only call if they have an inventory open
+ if (this.containerMenu != this.inventoryMenu) {
+ this.closeContainer();
+ }
+
+- String deathMessage = event.getDeathMessage();
++ net.kyori.adventure.text.Component deathMessage = event.deathMessage() != null ? event.deathMessage() : net.kyori.adventure.text.Component.empty(); // Paper - Adventure
+
+- if (deathMessage != null && deathMessage.length() > 0 && flag) { // TODO: allow plugins to override?
+- Component ichatbasecomponent;
+- if (deathMessage.equals(deathmessage)) {
+- ichatbasecomponent = this.getCombatTracker().getDeathMessage();
+- } else {
+- ichatbasecomponent = org.bukkit.craftbukkit.util.CraftChatMessage.fromStringOrNull(deathMessage);
+- }
++ if (deathMessage != null && deathMessage != net.kyori.adventure.text.Component.empty() && flag) { // Paper - Adventure // TODO: allow plugins to override?
++ Component ichatbasecomponent = PaperAdventure.asVanilla(deathMessage); // Paper - Adventure
+
+ this.connection.send(new ClientboundPlayerCombatKillPacket(this.getId(), ichatbasecomponent), PacketSendListener.exceptionallySend(() -> {
+ boolean flag1 = true;
+@@ -2269,8 +2268,13 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ }
+
+ public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params) {
++ // Paper start
++ this.sendChatMessage(message, filterMaskEnabled, params, null);
++ }
++ public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params, @Nullable Component unsigned) {
++ // Paper end
+ if (this.acceptsChatMessages()) {
+- message.sendToPlayer(this, filterMaskEnabled, params);
++ message.sendToPlayer(this, filterMaskEnabled, params, unsigned); // Paper
+ }
+
+ }
+@@ -2297,6 +2301,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ }
+ // CraftBukkit end
+ this.language = clientOptions.language();
++ this.adventure$locale = java.util.Objects.requireNonNullElse(net.kyori.adventure.translation.Translator.parseLocale(this.language), java.util.Locale.US); // Paper
+ this.requestedViewDistance = clientOptions.viewDistance();
+ this.chatVisibility = clientOptions.chatVisibility();
+ this.canChatColor = clientOptions.chatColors();
+diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+index 64b13cc2f32ab59dd6bea6d33c475228384b7c3e..99f89854e43ed6742dc9ac1624fa7140b4594b3b 100644
+--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+@@ -73,7 +73,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ private static final Component TIMEOUT_DISCONNECTION_MESSAGE = Component.translatable("disconnect.timeout");
+ static final Component DISCONNECT_UNEXPECTED_QUERY = Component.translatable("multiplayer.disconnect.unexpected_query_response");
+ protected final MinecraftServer server;
+- protected final Connection connection;
++ public final Connection connection; // Paper
+ private final boolean transferred;
+ private long keepAliveTime;
+ private boolean keepAlivePending;
+@@ -82,6 +82,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ private boolean closed = false;
+ private int latency;
+ private volatile boolean suspendFlushingOnServerThread = false;
++ public final java.util.Map<java.util.UUID, net.kyori.adventure.resource.ResourcePackCallback> packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks
+
+ public ServerCommonPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { // CraftBukkit
+ this.server = minecraftserver;
+@@ -201,6 +202,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ ServerCommonPacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack {} rejection", this.playerProfile().getName(), packet.id());
+ this.disconnect((Component) Component.translatable("multiplayer.requiredTexturePrompt.disconnect"));
+ }
++ // Paper start - adventure pack callbacks
++ // call the callbacks before the previously-existing event so the event has final say
++ final net.kyori.adventure.resource.ResourcePackCallback callback;
++ if (packet.action().isTerminal()) {
++ callback = this.packCallbacks.remove(packet.id());
++ } else {
++ callback = this.packCallbacks.get(packet.id());
++ }
++ if (callback != null) {
++ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), this.getCraftPlayer());
++ }
++ // Paper end
+ this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()])); // CraftBukkit
+
+ }
+@@ -287,6 +300,12 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ }
+ }
+
++ // Paper start - adventure
++ public void disconnect(final net.kyori.adventure.text.Component reason) {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason));
++ }
++ // Paper end - adventure
++
+ public void disconnect(Component reason) {
+ this.disconnect(new DisconnectionDetails(reason));
+ }
+@@ -317,9 +336,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ return;
+ }
+
+- String leaveMessage = ChatFormatting.YELLOW + this.player.getScoreboardName() + " left the game.";
++ net.kyori.adventure.text.Component leaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? this.player.getBukkitEntity().displayName() : net.kyori.adventure.text.Component.text(this.player.getScoreboardName())); // Paper - Adventure
+
+- PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), CraftChatMessage.fromComponent(disconnectionInfo.reason()), leaveMessage);
++ PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionInfo.reason()), leaveMessage); // Paper - adventure
+
+ if (this.cserver.getServer().isRunning()) {
+ this.cserver.getPluginManager().callEvent(event);
+@@ -331,7 +350,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ }
+ this.player.kickLeaveMessage = event.getLeaveMessage(); // CraftBukkit - SPIGOT-3034: Forward leave message to PlayerQuitEvent
+ // Send the possibly modified leave message
+- this.disconnect0(new DisconnectionDetails(CraftChatMessage.fromString(event.getReason(), true)[0], disconnectionInfo.report(), disconnectionInfo.bugReportLink()));
++ this.disconnect0(new DisconnectionDetails(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason()), disconnectionInfo.report(), disconnectionInfo.bugReportLink())); // Paper - Adventure
+ }
+
+ private void disconnect0(DisconnectionDetails disconnectiondetails) {
+diff --git a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
+index e7c407039fef88ef01ba9b6be9ae5bcc3edc026f..5457358bc76889153036818fdfd70a043ec4e40f 100644
+--- a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
+@@ -120,6 +120,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis
+ @Override
+ public void handleClientInformation(ServerboundClientInformationPacket packet) {
+ this.clientInformation = packet.information();
++ this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index 2394ff9873590bd9d4f377f60973b8c098144857..ef98348a701efb10d65414f9ab2acd640900d24f 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -45,6 +45,7 @@ import net.minecraft.nbt.CompoundTag;
+ import net.minecraft.network.Connection;
+ import net.minecraft.network.DisconnectionDetails;
+ import net.minecraft.network.TickablePacketListener;
++import net.minecraft.network.chat.ChatDecorator;
+ import net.minecraft.network.chat.ChatType;
+ import net.minecraft.network.chat.Component;
+ import net.minecraft.network.chat.LastSeenMessages;
+@@ -203,6 +204,8 @@ import net.minecraft.world.phys.shapes.VoxelShape;
+ import org.slf4j.Logger;
+
+ // CraftBukkit start
++import io.papermc.paper.adventure.ChatProcessor; // Paper
++import io.papermc.paper.adventure.PaperAdventure; // Paper
+ import com.mojang.datafixers.util.Pair;
+ import java.util.Arrays;
+ import java.util.concurrent.ExecutionException;
+@@ -1814,9 +1817,11 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ */
+
+ this.player.disconnect();
+- String quitMessage = this.server.getPlayerList().remove(this.player);
+- if ((quitMessage != null) && (quitMessage.length() > 0)) {
+- this.server.getPlayerList().broadcastMessage(CraftChatMessage.fromString(quitMessage));
++ // Paper start - Adventure
++ net.kyori.adventure.text.Component quitMessage = this.server.getPlayerList().remove(this.player);
++ if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) {
++ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false);
++ // Paper end
+ }
+ // CraftBukkit end
+ this.player.getTextFilter().leave();
+@@ -1877,10 +1882,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ CompletableFuture<FilteredText> completablefuture = this.filterTextPacket(playerchatmessage.signedContent()).thenApplyAsync(Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat
+- Component ichatbasecomponent = this.server.getChatDecorator().decorate(this.player, playerchatmessage.decoratedContent());
++ CompletableFuture<Component> componentFuture = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage.decoratedContent()); // Paper - Adventure
+
+- this.chatMessageChain.append(completablefuture, (filteredtext) -> {
+- PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(ichatbasecomponent).filter(filteredtext.mask());
++ this.chatMessageChain.append(CompletableFuture.allOf(completablefuture, componentFuture), (filteredtext) -> { // Paper - Adventure
++ PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(componentFuture.join()).filter(completablefuture.join().mask()); // Paper - Adventure
+
+ this.broadcastChatMessage(playerchatmessage1);
+ });
+@@ -2100,7 +2105,15 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ this.handleCommand(s);
+ } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
+ // Do nothing, this is coming from a plugin
+- } else {
++ // Paper start
++ } else if (true) {
++ if (!async && !org.bukkit.Bukkit.isPrimaryThread()) {
++ org.spigotmc.AsyncCatcher.catchOp("Asynchronous player chat is not allowed here");
++ }
++ final ChatProcessor cp = new ChatProcessor(this.server, this.player, original, async);
++ cp.process();
++ // Paper end
++ } else if (false) { // Paper
+ Player player = this.getCraftPlayer();
+ AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server));
+ String originalFormat = event.getFormat(), originalMessage = event.getMessage();
+@@ -3129,6 +3142,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ boolean flag = this.player.isModelPartShown(PlayerModelPart.HAT);
+
+ this.player.updateOptions(packet.information());
++ this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
+ if (this.player.isModelPartShown(PlayerModelPart.HAT) != flag) {
+ this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT, this.player));
+ }
+diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+index c209ac2be7fabcd36cfcc0400308e44a26d78263..8cf3b9f1b7eef2d6278830e21ae012852687e02b 100644
+--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+@@ -338,7 +338,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+ if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) {
+ final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId);
+ if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
+- event.disallow(asyncEvent.getResult(), asyncEvent.getKickMessage());
++ event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
+ }
+ Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<PlayerPreLoginEvent.Result>() {
+ @Override
+@@ -350,12 +350,12 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+
+ ServerLoginPacketListenerImpl.this.server.processQueue.add(waitable);
+ if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) {
+- this.disconnect(event.getKickMessage());
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
+ return;
+ }
+ } else {
+ if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
+- this.disconnect(asyncEvent.getKickMessage());
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(asyncEvent.kickMessage())); // Paper - Adventure
+ return;
+ }
+ }
+diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
+index 3848cc836785829c5aa82bdb5d37d36a97f94a04..f08700abb005f487aca95c0457c09cefa9a81be2 100644
+--- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
+@@ -58,7 +58,7 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene
+ CraftIconCache icon = server.server.getServerIcon();
+
+ ServerListPingEvent() {
+- super(ServerStatusPacketListenerImpl.this.connection.hostname, ((InetSocketAddress) ServerStatusPacketListenerImpl.this.connection.getRemoteAddress()).getAddress(), server.getMotd(), server.getPlayerList().getMaxPlayers());
++ super(ServerStatusPacketListenerImpl.this.connection.hostname, ((InetSocketAddress) ServerStatusPacketListenerImpl.this.connection.getRemoteAddress()).getAddress(), server.server.motd(), server.getPlayerList().getMaxPlayers()); // Paper - Adventure
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 7bb87d2bdf0ead0fdca38a9685e2e15b249ec2cb..1333daa8666fe2ec4033a2f57ba6b716fcdd5343 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -274,7 +274,7 @@ public abstract class PlayerList {
+ }
+ // CraftBukkit start
+ ichatmutablecomponent.withStyle(ChatFormatting.YELLOW);
+- String joinMessage = CraftChatMessage.fromComponent(ichatmutablecomponent);
++ Component joinMessage = ichatmutablecomponent; // Paper - Adventure
+
+ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
+ ServerStatus serverping = this.server.getStatus();
+@@ -295,19 +295,18 @@ public abstract class PlayerList {
+ // Ensure that player inventory is populated with its viewer
+ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer);
+
+- PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, joinMessage);
++ PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
+ this.cserver.getPluginManager().callEvent(playerJoinEvent);
+
+ if (!player.connection.isAcceptingMessages()) {
+ return;
+ }
+
+- joinMessage = playerJoinEvent.getJoinMessage();
++ final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
+
+- if (joinMessage != null && joinMessage.length() > 0) {
+- for (Component line : org.bukkit.craftbukkit.util.CraftChatMessage.fromString(joinMessage)) {
+- this.server.getPlayerList().broadcastSystemMessage(line, false);
+- }
++ if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure
++ joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure
++ this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure
+ }
+ // CraftBukkit end
+
+@@ -451,7 +450,7 @@ public abstract class PlayerList {
+
+ }
+
+- public String remove(ServerPlayer entityplayer) { // CraftBukkit - return string
++ public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component
+ ServerLevel worldserver = entityplayer.serverLevel();
+
+ entityplayer.awardStat(Stats.LEAVE_GAME);
+@@ -462,7 +461,7 @@ public abstract class PlayerList {
+ entityplayer.closeContainer();
+ }
+
+- PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), entityplayer.kickLeaveMessage != null ? entityplayer.kickLeaveMessage : "\u00A7e" + entityplayer.getScoreboardName() + " left the game");
++ PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); // Paper - Adventure
+ this.cserver.getPluginManager().callEvent(playerQuitEvent);
+ entityplayer.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage());
+
+@@ -523,7 +522,7 @@ public abstract class PlayerList {
+ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity());
+ // CraftBukkit end
+
+- return playerQuitEvent.getQuitMessage(); // CraftBukkit
++ return playerQuitEvent.quitMessage(); // Paper - Adventure
+ }
+
+ // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer
+@@ -570,11 +569,11 @@ public abstract class PlayerList {
+ }
+
+ // return chatmessage;
+- event.disallow(PlayerLoginEvent.Result.KICK_BANNED, CraftChatMessage.fromComponent(ichatmutablecomponent));
++ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
+ } else if (!this.isWhiteListed(gameprofile)) {
+ ichatmutablecomponent = Component.translatable("multiplayer.disconnect.not_whitelisted");
+- event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, org.spigotmc.SpigotConfig.whitelistMessage); // Spigot
+- } else if (this.ipBans.isBanned(socketaddress)) {
++ event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure
++ } else if (this.getIpBans().isBanned(socketaddress) && !this.getIpBans().get(socketaddress).hasExpired()) {
+ IpBanListEntry ipbanentry = this.ipBans.get(socketaddress);
+
+ ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason());
+@@ -583,17 +582,17 @@ public abstract class PlayerList {
+ }
+
+ // return chatmessage;
+- event.disallow(PlayerLoginEvent.Result.KICK_BANNED, CraftChatMessage.fromComponent(ichatmutablecomponent));
++ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
+ } else {
+ // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null;
+ if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) {
+- event.disallow(PlayerLoginEvent.Result.KICK_FULL, org.spigotmc.SpigotConfig.serverFullMessage); // Spigot
++ event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure
+ }
+ }
+
+ this.cserver.getPluginManager().callEvent(event);
+ if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) {
+- loginlistener.disconnect(event.getKickMessage());
++ loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
+ return null;
+ }
+ return entity;
+@@ -1091,7 +1090,7 @@ public abstract class PlayerList {
+ public void removeAll() {
+ // CraftBukkit start - disconnect safely
+ for (ServerPlayer player : this.players) {
+- player.connection.disconnect(CraftChatMessage.fromStringOrEmpty(this.server.server.getShutdownMessage())); // CraftBukkit - add custom shutdown message
++ player.connection.disconnect(java.util.Objects.requireNonNullElseGet(this.server.server.shutdownMessage(), net.kyori.adventure.text.Component::empty)); // CraftBukkit - add custom shutdown message // Paper - Adventure
+ }
+ // CraftBukkit end
+
+@@ -1132,24 +1131,43 @@ public abstract class PlayerList {
+ }
+
+ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params) {
++ // Paper start
++ this.broadcastChatMessage(message, sender, params, null);
++ }
++ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
++ // Paper end
+ Objects.requireNonNull(sender);
+- this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params);
++ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params, unsignedFunction); // Paper
+ }
+
+ private void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params) {
++ // Paper start
++ this.broadcastChatMessage(message, shouldSendFiltered, sender, params, null);
++ }
++ public void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
++ // Paper end
+ boolean flag = this.verifyChatTrusted(message);
+
+- this.server.logChatMessage(message.decoratedContent(), params, flag ? null : "Not Secure");
++ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), params, flag ? null : "Not Secure"); // Paper
+ OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message);
+ boolean flag1 = false;
+
+ boolean flag2;
++ Packet<?> disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingchatmessage.content(), params) : null; // Paper - don't send player chat packets from vanished players
+
+ for (Iterator iterator = this.players.iterator(); iterator.hasNext(); flag1 |= flag2 && message.isFullyFiltered()) {
+ ServerPlayer entityplayer1 = (ServerPlayer) iterator.next();
+
+ flag2 = shouldSendFiltered.test(entityplayer1);
+- entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params);
++ // Paper start - don't send player chat packets from vanished players
++ if (sender != null && !entityplayer1.getBukkitEntity().canSee(sender.getBukkitEntity())) {
++ entityplayer1.connection.send(unsignedFunction != null
++ ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(entityplayer1.getBukkitEntity()), params)
++ : disguised);
++ continue;
++ }
++ // Paper end
++ entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params, unsignedFunction == null ? null : unsignedFunction.apply(entityplayer1.getBukkitEntity())); // Paper
+ }
+
+ if (flag1 && sender != null) {
+@@ -1158,7 +1176,7 @@ public abstract class PlayerList {
+
+ }
+
+- private boolean verifyChatTrusted(PlayerChatMessage message) {
++ public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public
+ return message.hasSignature() && !message.hasExpiredServer(Instant.now());
+ }
+
+diff --git a/src/main/java/net/minecraft/world/BossEvent.java b/src/main/java/net/minecraft/world/BossEvent.java
+index ed54c81a3269360acce674aa4e1d54ccb2461841..c9c849534c3998cfcab7ddcb12a71ccb1fdb3e1a 100644
+--- a/src/main/java/net/minecraft/world/BossEvent.java
++++ b/src/main/java/net/minecraft/world/BossEvent.java
+@@ -13,6 +13,7 @@ public abstract class BossEvent {
+ protected boolean darkenScreen;
+ protected boolean playBossMusic;
+ protected boolean createWorldFog;
++ public net.kyori.adventure.bossbar.BossBar adventure; // Paper
+
+ public BossEvent(UUID uuid, Component name, BossEvent.BossBarColor color, BossEvent.BossBarOverlay style) {
+ this.id = uuid;
+@@ -27,61 +28,75 @@ public abstract class BossEvent {
+ }
+
+ public Component getName() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.name()); // Paper
+ return this.name;
+ }
+
+ public void setName(Component name) {
++ if (this.adventure != null) this.adventure.name(io.papermc.paper.adventure.PaperAdventure.asAdventure(name)); // Paper
+ this.name = name;
+ }
+
+ public float getProgress() {
++ if (this.adventure != null) return this.adventure.progress(); // Paper
+ return this.progress;
+ }
+
+ public void setProgress(float percent) {
++ if (this.adventure != null) this.adventure.progress(percent); // Paper
+ this.progress = percent;
+ }
+
+ public BossEvent.BossBarColor getColor() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.color()); // Paper
+ return this.color;
+ }
+
+ public void setColor(BossEvent.BossBarColor color) {
++ if (this.adventure != null) this.adventure.color(io.papermc.paper.adventure.PaperAdventure.asAdventure(color)); // Paper
+ this.color = color;
+ }
+
+ public BossEvent.BossBarOverlay getOverlay() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.overlay()); // Paper
+ return this.overlay;
+ }
+
+ public void setOverlay(BossEvent.BossBarOverlay style) {
++ if (this.adventure != null) this.adventure.overlay(io.papermc.paper.adventure.PaperAdventure.asAdventure(style)); // Paper
+ this.overlay = style;
+ }
+
+ public boolean shouldDarkenScreen() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN); // Paper
+ return this.darkenScreen;
+ }
+
+ public BossEvent setDarkenScreen(boolean darkenSky) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN, darkenSky); // Paper
+ this.darkenScreen = darkenSky;
+ return this;
+ }
+
+ public boolean shouldPlayBossMusic() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC); // Paper
+ return this.playBossMusic;
+ }
+
+ public BossEvent setPlayBossMusic(boolean dragonMusic) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC, dragonMusic); // Paper
+ this.playBossMusic = dragonMusic;
+ return this;
+ }
+
+ public BossEvent setCreateWorldFog(boolean thickenFog) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG, thickenFog); // Paper
+ this.createWorldFog = thickenFog;
+ return this;
+ }
+
+ public boolean shouldCreateWorldFog() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG); // Paper
+ return this.createWorldFog;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
+index 5fa11da0e1bfb0f0030746d1f58e40f09c04a221..1db50e72ad8e04e54bed9b462bd7276feb06ce4c 100644
+--- a/src/main/java/net/minecraft/world/item/ItemStack.java
++++ b/src/main/java/net/minecraft/world/item/ItemStack.java
+@@ -184,7 +184,15 @@ public final class ItemStack implements DataComponentHolder {
+ CraftItemStack.setItemMeta(itemstack, CraftItemStack.getItemMeta(itemstack));
+ // Spigot end
+ ITEM_STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.getItemHolder()); // CraftBukkit - decompile error
++ // Paper start - adventure; conditionally render translatable components
++ boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get();
++ try {
++ net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true);
+ DataComponentPatch.STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.components.asPatch());
++ } finally {
++ net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev);
++ }
++ // Paper end - adventure; conditionally render translatable components
+ }
+ }
+ };
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java
+index 56455d56b9657a31bccb8fdc0cff02b5850414fd..9e185afc561a4470055bba7dd1d256ee83805c8d 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/SignBlockEntity.java
+@@ -210,22 +210,22 @@ public class SignBlockEntity extends BlockEntity {
+
+ // CraftBukkit start
+ Player player = ((ServerPlayer) entityhuman).getBukkitEntity();
+- String[] lines = new String[4];
++ List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>(); // Paper - adventure
+
+ for (int i = 0; i < list.size(); ++i) {
+- lines[i] = CraftChatMessage.fromComponent(signtext.getMessage(i, entityhuman.isTextFilteringEnabled()));
++ lines.add(io.papermc.paper.adventure.PaperAdventure.asAdventure(signtext.getMessage(i, entityhuman.isTextFilteringEnabled()))); // Paper - Adventure
+ }
+
+- SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, lines.clone(), (front) ? Side.FRONT : Side.BACK);
++ SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, new java.util.ArrayList<>(lines), (front) ? Side.FRONT : Side.BACK); // Paper - Adventure
+ entityhuman.level().getCraftServer().getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+ return originalText;
+ }
+
+- Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.getLines());
++ Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.lines()); // Paper - Adventure
+ for (int i = 0; i < components.length; i++) {
+- if (!Objects.equals(lines[i], event.getLine(i))) {
++ if (!Objects.equals(lines.get(i), event.line(i))) { // Paper - Adventure
+ signtext = signtext.setMessage(i, components[i]);
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+index 2decbf562161fa51b931fcb208a3503d7663bb2e..c21ae4975206398e7d20b37a749b830b9219c746 100644
+--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+@@ -48,6 +48,7 @@ import net.minecraft.world.level.saveddata.SavedData;
+ import org.slf4j.Logger;
+
+ // CraftBukkit start
++import io.papermc.paper.adventure.PaperAdventure; // Paper
+ import java.util.UUID;
+ import org.bukkit.Bukkit;
+ import org.bukkit.craftbukkit.CraftServer;
+@@ -650,7 +651,7 @@ public class MapItemSavedData extends SavedData {
+
+ for (org.bukkit.map.MapCursor cursor : render.cursors) {
+ if (cursor.isVisible()) {
+- icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), CraftChatMessage.fromStringOrOptional(cursor.getCaption())));
++ icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption()))));
+ }
+ }
+ collection = icons;
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftJukeboxSong.java b/src/main/java/org/bukkit/craftbukkit/CraftJukeboxSong.java
+index 49c037e961c5ca5ba8d6a870cb32ffe8719adc91..2772c19f58a35713d61aab24f6f0d6f5070153c6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftJukeboxSong.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftJukeboxSong.java
+@@ -59,6 +59,7 @@ public class CraftJukeboxSong implements JukeboxSong, Handleable<net.minecraft.w
+ @NotNull
+ @Override
+ public String getTranslationKey() {
++ if (!(this.handle.description().getContents() instanceof TranslatableContents)) throw new UnsupportedOperationException("Description isn't translatable!"); // Paper
+ return ((TranslatableContents) this.handle.description().getContents()).getKey();
+ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index e5054699f2f7555455b4da20249e253dba7043b4..c3774d9a253d4fda80f63d4040722ab5c1c94be4 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -648,8 +648,10 @@ public final class CraftServer implements Server {
+ }
+
+ @Override
++ @Deprecated // Paper start
+ public int broadcastMessage(String message) {
+ return this.broadcast(message, BROADCAST_CHANNEL_USERS);
++ // Paper end
+ }
+
+ @Override
+@@ -1627,7 +1629,15 @@ public final class CraftServer implements Server {
+ return this.configuration.getInt("settings.spawn-radius", -1);
+ }
+
++ // Paper start
+ @Override
++ public net.kyori.adventure.text.Component shutdownMessage() {
++ String msg = getShutdownMessage();
++ return msg != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(msg) : null;
++ }
++ // Paper end
++ @Override
++ @Deprecated // Paper
+ public String getShutdownMessage() {
+ return this.configuration.getString("settings.shutdown-message");
+ }
+@@ -1801,7 +1811,20 @@ public final class CraftServer implements Server {
+ }
+
+ @Override
++ @Deprecated // Paper
+ public int broadcast(String message, String permission) {
++ // Paper start - Adventure
++ return this.broadcast(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(message), permission);
++ }
++
++ @Override
++ public int broadcast(net.kyori.adventure.text.Component message) {
++ return this.broadcast(message, BROADCAST_CHANNEL_USERS);
++ }
++
++ @Override
++ public int broadcast(net.kyori.adventure.text.Component message, String permission) {
++ // Paper end
+ Set<CommandSender> recipients = new HashSet<>();
+ for (Permissible permissible : this.getPluginManager().getPermissionSubscriptions(permission)) {
+ if (permissible instanceof CommandSender && permissible.hasPermission(permission)) {
+@@ -1809,14 +1832,14 @@ public final class CraftServer implements Server {
+ }
+ }
+
+- BroadcastMessageEvent broadcastMessageEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients);
++ BroadcastMessageEvent broadcastMessageEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients); // Paper - Adventure
+ this.getPluginManager().callEvent(broadcastMessageEvent);
+
+ if (broadcastMessageEvent.isCancelled()) {
+ return 0;
+ }
+
+- message = broadcastMessageEvent.getMessage();
++ message = broadcastMessageEvent.message(); // Paper - Adventure
+
+ for (CommandSender recipient : recipients) {
+ recipient.sendMessage(message);
+@@ -2078,6 +2101,14 @@ public final class CraftServer implements Server {
+ return CraftInventoryCreator.INSTANCE.createInventory(owner, type);
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ Preconditions.checkArgument(type.isCreatable(), "Cannot open an inventory of type ", type);
++ return CraftInventoryCreator.INSTANCE.createInventory(owner, type, title);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) {
+ Preconditions.checkArgument(type != null, "InventoryType cannot be null");
+@@ -2092,13 +2123,28 @@ public final class CraftServer implements Server {
+ return CraftInventoryCreator.INSTANCE.createInventory(owner, size);
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) throws IllegalArgumentException {
++ Preconditions.checkArgument(9 <= size && size <= 54 && size % 9 == 0, "Size for custom inventory must be a multiple of 9 between 9 and 54 slots (got " + size + ")");
++ return CraftInventoryCreator.INSTANCE.createInventory(owner, size, title);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder owner, int size, String title) throws IllegalArgumentException {
+ Preconditions.checkArgument(9 <= size && size <= 54 && size % 9 == 0, "Size for custom inventory must be a multiple of 9 between 9 and 54 slots (got %s)", size);
+ return CraftInventoryCreator.INSTANCE.createInventory(owner, size, title);
+ }
+
++ // Paper start
+ @Override
++ public Merchant createMerchant(net.kyori.adventure.text.Component title) {
++ return new org.bukkit.craftbukkit.inventory.CraftMerchantCustom(title == null ? InventoryType.MERCHANT.defaultTitle() : title);
++ }
++ // Paper end
++ @Override
++ @Deprecated // Paper
+ public Merchant createMerchant(String title) {
+ return new CraftMerchantCustom(title == null ? InventoryType.MERCHANT.getDefaultTitle() : title);
+ }
+@@ -2163,6 +2209,17 @@ public final class CraftServer implements Server {
+ return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog)
+ }
+
++ // Paper start - Adventure
++ @Override
++ public net.kyori.adventure.text.Component motd() {
++ return this.console.motd();
++ }
++ @Override
++ public void motd(final net.kyori.adventure.text.Component motd) {
++ this.console.motd(motd);
++ }
++ // Paper end
++
+ @Override
+ public String getMotd() {
+ return this.console.getMotd();
+@@ -2632,4 +2689,57 @@ public final class CraftServer implements Server {
+ public double[] getTPS() {
+ return new double[]{0, 0, 0}; // TODO
+ }
++
++ // Paper start - adventure sounds
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound) {
++ if (sound.seed().isEmpty()) org.spigotmc.AsyncCatcher.catchOp("play sound; cannot generate seed with world random"); // Paper
++ final long seed = sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong);
++ for (ServerPlayer player : this.playerList.getPlayers()) {
++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null));
++ }
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) {
++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong), this.playSound0(x, y, z, this.console.getAllLevels()));
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) {
++ if (sound.seed().isEmpty()) org.spigotmc.AsyncCatcher.catchOp("play sound; cannot generate seed with world random"); // Paper
++ final long seed = sound.seed().orElseGet(this.console.overworld().getRandom()::nextLong);
++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) {
++ for (ServerPlayer player : this.playerList.getPlayers()) {
++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null));
++ }
++ } else if (emitter instanceof org.bukkit.craftbukkit.entity.CraftEntity craftEntity) {
++ org.spigotmc.AsyncCatcher.catchOp("play sound; cannot use entity emitter"); // Paper
++ final net.minecraft.world.entity.Entity entity = craftEntity.getHandle();
++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, seed, this.playSound0(entity.getX(), entity.getY(), entity.getZ(), List.of((ServerLevel) entity.level())));
++ } else {
++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter);
++ }
++ }
++
++ private java.util.function.BiConsumer<net.minecraft.network.protocol.Packet<?>, Float> playSound0(final double x, final double y, final double z, final Iterable<ServerLevel> levels) {
++ return (packet, distance) -> {
++ for (final ServerLevel level : levels) {
++ level.getServer().getPlayerList().broadcast(null, x, y, z, distance, level.dimension(), packet);
++ }
++ };
++ }
++ // Paper end
++
++ // Paper start
++ private Iterable<? extends net.kyori.adventure.audience.Audience> adventure$audiences;
++ @Override
++ public Iterable<? extends net.kyori.adventure.audience.Audience> audiences() {
++ if (this.adventure$audiences == null) {
++ this.adventure$audiences = com.google.common.collect.Iterables.concat(java.util.Collections.singleton(this.getConsoleSender()), this.getOnlinePlayers());
++ }
++ return this.adventure$audiences;
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServerLinks.java b/src/main/java/org/bukkit/craftbukkit/CraftServerLinks.java
+index cbdb1a56a97150c164515a4ce6d3ba06428bf321..b214e7b302abbfe1641485a05f1371ac65ffb517 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServerLinks.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServerLinks.java
+@@ -61,6 +61,19 @@ public class CraftServerLinks implements ServerLinks {
+ return link;
+ }
+
++ // Paper start - Adventure
++ @Override
++ public ServerLink addLink(net.kyori.adventure.text.Component displayName, URI url) {
++ Preconditions.checkArgument(displayName != null, "displayName cannot be null");
++ Preconditions.checkArgument(url != null, "url cannot be null");
++
++ CraftServerLink link = new CraftServerLink(net.minecraft.server.ServerLinks.Entry.custom(io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName), url));
++ this.addLink(link);
++
++ return link;
++ }
++ // Paper end - Adventure
++
+ @Override
+ public ServerLink addLink(String displayName, URI url) {
+ Preconditions.checkArgument(displayName != null, "displayName cannot be null");
+@@ -134,6 +147,13 @@ public class CraftServerLinks implements ServerLinks {
+ return CraftChatMessage.fromComponent(this.handle.displayName());
+ }
+
++ // Paper start - Adventure
++ @Override
++ public net.kyori.adventure.text.Component displayName() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.handle.displayName());
++ }
++ // Paper end - Adventure
++
+ @Override
+ public URI getUrl() {
+ return this.handle.link();
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index 8e5a6137321d1d4941de8be2af5c7a3e5e143cf1..1827df86bf21ca3ed297fd4316f3a962063a47fb 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -167,6 +167,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ private final BlockMetadataStore blockMetadata = new BlockMetadataStore(this);
+ private final Object2IntOpenHashMap<SpawnCategory> spawnCategoryLimit = new Object2IntOpenHashMap<>();
+ private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftWorld.DATA_TYPE_REGISTRY);
++ private net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers
+
+ private static final Random rand = new Random();
+
+@@ -1710,6 +1711,15 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ entityTracker.broadcastAndSend(packet);
+ }
+ }
++ // Paper start - Adventure
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound) {
++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
++ final long seed = sound.seed().orElseGet(this.world.getRandom()::nextLong);
++ for (ServerPlayer player : this.getHandle().players()) {
++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null));
++ }
++ }
+
+ @Override
+ public void playSound(Entity entity, String sound, org.bukkit.SoundCategory category, float volume, float pitch, long seed) {
+@@ -1722,6 +1732,33 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ }
+ }
+
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) {
++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.world.getRandom()::nextLong), this.playSound0(x, y, z));
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) {
++ org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper
++ final long seed = sound.seed().orElseGet(this.getHandle().getRandom()::nextLong);
++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) {
++ for (ServerPlayer player : this.getHandle().players()) {
++ player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null));
++ }
++ } else if (emitter instanceof CraftEntity craftEntity) {
++ final net.minecraft.world.entity.Entity entity = craftEntity.getHandle();
++ io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, seed, this.playSound0(entity.getX(), entity.getY(), entity.getZ()));
++ } else {
++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter);
++ }
++ }
++
++ private java.util.function.BiConsumer<net.minecraft.network.protocol.Packet<?>, Float> playSound0(final double x, final double y, final double z) {
++ return (packet, distance) -> this.world.getServer().getPlayerList().broadcast(null, x, y, z, distance, this.world.dimension(), packet);
++ }
++ // Paper end - Adventure
++
+ private Map<String, GameRules.Key<?>> gamerules;
+ public synchronized Map<String, GameRules.Key<?>> getGameRulesNMS() {
+ if (this.gamerules != null) {
+@@ -2161,5 +2198,18 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ public void setSendViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
++
++ // Paper start - implement pointers
++ @Override
++ public net.kyori.adventure.pointer.Pointers pointers() {
++ if (this.adventure$pointers == null) {
++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder()
++ .withDynamic(net.kyori.adventure.identity.Identity.NAME, this::getName)
++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUID)
++ .build();
++ }
++
++ return this.adventure$pointers;
++ }
+ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index d2aa1d32a62e074b53f304a755d42687ba0422ee..c210d21382e0922aaf61f2c51949f753e6462b9e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -20,6 +20,12 @@ public class Main {
+ public static boolean useConsole = true;
+
+ public static void main(String[] args) {
++ // Paper start
++ final String warnWhenLegacyFormattingDetected = String.join(".", "net", "kyori", "adventure", "text", "warnWhenLegacyFormattingDetected");
++ if (false && System.getProperty(warnWhenLegacyFormattingDetected) == null) {
++ System.setProperty(warnWhenLegacyFormattingDetected, String.valueOf(true));
++ }
++ // Paper end
+ // Todo: Installation script
+ OptionParser parser = new OptionParser() {
+ {
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java
+index 0949f9e6bcb66da94c30439ce4ed4c8415537526..8021ac39cb9c1ff45123d51e6f13b840d1290bb2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBeacon.java
+@@ -81,6 +81,19 @@ public class CraftBeacon extends CraftBlockEntityState<BeaconBlockEntity> implem
+ this.getSnapshot().secondaryPower = (effect != null) ? CraftPotionEffectType.bukkitToMinecraftHolder(effect) : null;
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component customName() {
++ final BeaconBlockEntity be = this.getSnapshot();
++ return be.name != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.name) : null;
++ }
++
++ @Override
++ public void customName(final net.kyori.adventure.text.Component customName) {
++ this.getSnapshot().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null);
++ }
++ // Paper end
++
+ @Override
+ public String getCustomName() {
+ BeaconBlockEntity beacon = this.getSnapshot();
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java
+index f9b89a7c6ac9f7fdbd29567a5b6550398dbc7345..f5b0bec4c1164fe7ef6da1f19a6ce9bb3d6864d0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java
+@@ -45,4 +45,16 @@ public class CraftCommandBlock extends CraftBlockEntityState<CommandBlockEntity>
+ public CraftCommandBlock copy(Location location) {
+ return new CraftCommandBlock(this, location);
+ }
++
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component name() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(getSnapshot().getCommandBlock().getName());
++ }
++
++ @Override
++ public void name(net.kyori.adventure.text.Component name) {
++ getSnapshot().getCommandBlock().setCustomName(name == null ? net.minecraft.network.chat.Component.literal("@") : io.papermc.paper.adventure.PaperAdventure.asVanilla(name));
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java
+index 32f3c1f1903baf26c69b1262ff2956d7dcb84a90..76888df4b47ee0d134bca4a3aecddbb1bf4c09b0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftContainer.java
+@@ -57,6 +57,19 @@ public abstract class CraftContainer<T extends BaseContainerBlockEntity> extends
+ }
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component customName() {
++ final T be = this.getSnapshot();
++ return be.hasCustomName() ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.getCustomName()) : null;
++ }
++
++ @Override
++ public void customName(final net.kyori.adventure.text.Component customName) {
++ this.getSnapshot().name = (customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null);
++ }
++ // Paper end
++
+ @Override
+ public String getCustomName() {
+ T container = this.getSnapshot();
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java
+index 690dd79b82108322a290c00de63b1f038f617c84..a01691f98a378a818b8bf12176c7270e15c316d8 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftEnchantingTable.java
+@@ -16,6 +16,19 @@ public class CraftEnchantingTable extends CraftBlockEntityState<EnchantingTableB
+ super(state, location);
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component customName() {
++ final EnchantingTableBlockEntity be = this.getSnapshot();
++ return be.hasCustomName() ? io.papermc.paper.adventure.PaperAdventure.asAdventure(be.getCustomName()) : null;
++ }
++
++ @Override
++ public void customName(final net.kyori.adventure.text.Component customName) {
++ this.getSnapshot().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null);
++ }
++ // Paper end
++
+ @Override
+ public String getCustomName() {
+ EnchantingTableBlockEntity enchant = this.getSnapshot();
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java
+index 0641d17d8095e7700c651e90472ae53d6f7dbbb2..a12702cdf36c75572e661b5b5758270f5058c181 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java
+@@ -36,6 +36,23 @@ public class CraftSign<T extends SignBlockEntity> extends CraftBlockEntityState<
+ this.back = new CraftSignSide(this.getSnapshot().getBackText());
+ }
+
++ // Paper start
++ @Override
++ public java.util.@NotNull List<net.kyori.adventure.text.Component> lines() {
++ return this.front.lines();
++ }
++
++ @Override
++ public net.kyori.adventure.text.@NotNull Component line(int index) {
++ return this.front.line(index);
++ }
++
++ @Override
++ public void line(int index, net.kyori.adventure.text.@NotNull Component line) {
++ this.front.line(index, line);
++ }
++ // Paper end
++
+ @Override
+ public String[] getLines() {
+ return this.front.getLines();
+@@ -161,6 +178,20 @@ public class CraftSign<T extends SignBlockEntity> extends CraftBlockEntityState<
+ ((CraftPlayer) player).getHandle().openTextEdit(handle, Side.FRONT == side);
+ }
+
++ // Paper start
++ public static Component[] sanitizeLines(java.util.List<? extends net.kyori.adventure.text.Component> lines) {
++ Component[] components = new Component[4];
++ for (int i = 0; i < 4; i++) {
++ if (i < lines.size() && lines.get(i) != null) {
++ components[i] = io.papermc.paper.adventure.PaperAdventure.asVanilla(lines.get(i));
++ } else {
++ components[i] = net.minecraft.network.chat.Component.literal("");
++ }
++ }
++ return components;
++ }
++ // Paper end
++
+ public static Component[] sanitizeLines(String[] lines) {
+ Component[] components = new Component[4];
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java b/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java
+index d4724c812f8b7322ad59dc0695d01ceb47772dc4..4747d77fd5fd12116ef40d897a08c7baca60a399 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/sign/CraftSignSide.java
+@@ -12,37 +12,70 @@ import org.jetbrains.annotations.Nullable;
+ public class CraftSignSide implements SignSide {
+
+ // Lazily initialized only if requested:
+- private String[] originalLines = null;
+- private String[] lines = null;
++ // Paper start
++ private java.util.ArrayList<net.kyori.adventure.text.Component> originalLines = null; // ArrayList for RandomAccess
++ private java.util.ArrayList<net.kyori.adventure.text.Component> lines = null; // ArrayList for RandomAccess
++ // Paper end
+ private SignText signText;
+
+ public CraftSignSide(SignText signText) {
+ this.signText = signText;
+ }
+
++ // Paper start
++ @Override
++ public java.util.@NotNull List<net.kyori.adventure.text.Component> lines() {
++ this.loadLines();
++ return this.lines;
++ }
++
++ @Override
++ public net.kyori.adventure.text.@NotNull Component line(final int index) throws IndexOutOfBoundsException {
++ this.loadLines();
++ return this.lines.get(index);
++ }
++
++ @Override
++ public void line(final int index, final net.kyori.adventure.text.@NotNull Component line) throws IndexOutOfBoundsException {
++ com.google.common.base.Preconditions.checkArgument(line != null, "Line cannot be null");
++ this.loadLines();
++ this.lines.set(index, line);
++ }
++
++ private void loadLines() {
++ if (this.lines != null) {
++ return;
++ }
++ // Lazy initialization:
++ this.lines = io.papermc.paper.adventure.PaperAdventure.asAdventure(com.google.common.collect.Lists.newArrayList(this.signText.getMessages(false)));
++ this.originalLines = new java.util.ArrayList<>(this.lines);
++ }
++ // Paper end
++
+ @NotNull
+ @Override
+ public String[] getLines() {
+- if (this.lines == null) {
+- // Lazy initialization:
+- Component[] messages = this.signText.getMessages(false);
+- this.lines = new String[messages.length];
+- System.arraycopy(CraftSign.revertComponents(messages), 0, this.lines, 0, this.lines.length);
+- this.originalLines = new String[this.lines.length];
+- System.arraycopy(this.lines, 0, this.originalLines, 0, this.originalLines.length);
+- }
+- return this.lines;
++ // Paper start
++ this.loadLines();
++ return this.lines.stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::serialize).toArray(String[]::new); // Paper
++ // Paper end
+ }
+
+ @NotNull
+ @Override
+ public String getLine(int index) throws IndexOutOfBoundsException {
+- return this.getLines()[index];
++ // Paper start
++ this.loadLines();
++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.lines.get(index));
++ // Paper end
+ }
+
+ @Override
+ public void setLine(int index, @NotNull String line) throws IndexOutOfBoundsException {
+- this.getLines()[index] = line;
++ // Paper start
++ this.loadLines();
++ this.lines.set(index, line != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(line) : net.kyori.adventure.text.Component.empty());
++ // Paper end
+ }
+
+ @Override
+@@ -68,13 +101,16 @@ public class CraftSignSide implements SignSide {
+
+ public SignText applyLegacyStringToSignSide() {
+ if (this.lines != null) {
+- for (int i = 0; i < this.lines.length; i++) {
+- String line = (this.lines[i] == null) ? "" : this.lines[i];
+- if (line.equals(this.originalLines[i])) {
++ // Paper start
++ for (int i = 0; i < this.lines.size(); ++i) {
++ net.kyori.adventure.text.Component component = this.lines.get(i);
++ net.kyori.adventure.text.Component origComp = this.originalLines.get(i);
++ if (component.equals(origComp)) {
+ continue; // The line contents are still the same, skip.
+ }
+- this.signText = this.signText.setMessage(i, CraftChatMessage.fromString(line)[0]);
++ this.signText = this.signText.setMessage(i, io.papermc.paper.adventure.PaperAdventure.asVanilla(component));
+ }
++ // Paper end
+ }
+
+ return this.signText;
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java
+index f2e8b63d787754c0a92441dcc9eb39dffdc1e280..9feae61fe02cbc624581ef0bd4c60af636407367 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java
+@@ -61,6 +61,18 @@ public class CraftBlockCommandSender extends ServerCommandSender implements Bloc
+ return this.block.getTextName();
+ }
+
++ // Paper start
++ @Override
++ public void sendMessage(net.kyori.adventure.identity.Identity identity, net.kyori.adventure.text.Component message, net.kyori.adventure.audience.MessageType type) {
++ block.source.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message));
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component name() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.block.getDisplayName());
++ }
++ // Paper end
++
+ @Override
+ public boolean isOp() {
+ return CraftBlockCommandSender.SHARED_PERM.isOp();
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java
+index f3cb4102ab223f379f60dac317df7da1fab812a8..324e6d1a4fadd3e557e4ba05f04e6a5891cc54df 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java
+@@ -46,6 +46,13 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co
+ return "CONSOLE";
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component name() {
++ return net.kyori.adventure.text.Component.text(this.getName());
++ }
++ // Paper end
++
+ @Override
+ public boolean isOp() {
+ return true;
+@@ -80,4 +87,11 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co
+ public boolean isConversing() {
+ return this.conversationTracker.isConversing();
+ }
++
++ // Paper start
++ @Override
++ public void sendMessage(final net.kyori.adventure.identity.Identity identity, final net.kyori.adventure.text.Component message, final net.kyori.adventure.audience.MessageType type) {
++ this.sendRawMessage(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(message));
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java
+index e3c7fa50fad3077a297d2412de9d26d53371808c..5b7d230103f421fb939072e1526854f715430e51 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java
+@@ -39,6 +39,13 @@ public class CraftRemoteConsoleCommandSender extends ServerCommandSender impleme
+ return "Rcon";
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component name() {
++ return net.kyori.adventure.text.Component.text(this.getName());
++ }
++ // Paper end
++
+ @Override
+ public boolean isOp() {
+ return true;
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java
+index 53d6950ad270ba901de5226b9daecb683248ad05..3e7d14564f11a3ed0b0766444e9d681804597e9a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java
+@@ -67,6 +67,13 @@ public class ProxiedNativeCommandSender implements ProxiedCommandSender {
+ return this.getCallee().getName();
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component name() {
++ return this.getCallee().name();
++ }
++ // Paper end
++
+ @Override
+ public boolean isPermissionSet(String name) {
+ return this.getCaller().isPermissionSet(name);
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java
+index 1e82312c24cb752d63b165926861fc178cd7849b..7f22950ae61436e91a59cd29a345809c42bbe739 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/ServerCommandSender.java
+@@ -13,6 +13,7 @@ import org.bukkit.plugin.Plugin;
+
+ public abstract class ServerCommandSender implements CommandSender {
+ private final PermissibleBase perm;
++ private net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers
+
+ protected ServerCommandSender() {
+ this.perm = new PermissibleBase(this);
+@@ -130,4 +131,18 @@ public abstract class ServerCommandSender implements CommandSender {
+ return this.spigot;
+ }
+ // Spigot end
++
++ // Paper start - implement pointers
++ @Override
++ public net.kyori.adventure.pointer.Pointers pointers() {
++ if (this.adventure$pointers == null) {
++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder()
++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::name)
++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue)
++ .build();
++ }
++
++ return this.adventure$pointers;
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java
+index 2d0c82ca240e371a756d71f28e2e04d1aa8e6ad2..f73017bff613bd62b86c974b29576e241c24c927 100644
+--- a/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java
++++ b/src/main/java/org/bukkit/craftbukkit/enchantments/CraftEnchantment.java
+@@ -145,6 +145,12 @@ public class CraftEnchantment extends Enchantment implements Handleable<net.mine
+ CraftEnchantment ench = (CraftEnchantment) other;
+ return !net.minecraft.world.item.enchantment.Enchantment.areCompatible(this.handle, ench.handle);
+ }
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component displayName(int level) {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(getHandle().getFullname(level));
++ }
++ // Paper end
+
+ @Override
+ public String getTranslationKey() {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index 8b53a3c4f4285321b12ac0d51f94c04f39cc8ec8..978397e517a6fdb24c7d2b3f242545af07deeab0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -70,6 +70,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ private final EntityType entityType;
+ private EntityDamageEvent lastDamageEvent;
+ private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY);
++ protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers
+
+ public CraftEntity(final CraftServer server, final Entity entity) {
+ this.server = server;
+@@ -526,6 +527,32 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ return this.getHandle().getVehicle().getBukkitEntity();
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component customName() {
++ final Component name = this.getHandle().getCustomName();
++ return name != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(name) : null;
++ }
++
++ @Override
++ public void customName(final net.kyori.adventure.text.Component customName) {
++ this.getHandle().setCustomName(customName != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(customName) : null);
++ }
++
++ @Override
++ public net.kyori.adventure.pointer.Pointers pointers() {
++ if (this.adventure$pointers == null) {
++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder()
++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::name)
++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUniqueId)
++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue)
++ .build();
++ }
++
++ return this.adventure$pointers;
++ }
++ // Paper end
++
+ @Override
+ public void setCustomName(String name) {
+ // sane limit for name length
+@@ -622,6 +649,17 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ public String getName() {
+ return CraftChatMessage.fromComponent(this.getHandle().getName());
+ }
++ // Paper start
++ @Override
++ public [email protected] Component name() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getName());
++ }
++
++ @Override
++ public [email protected] Component teamDisplayName() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getDisplayName());
++ }
++ // Paper end
+
+ @Override
+ public boolean isPermissionSet(String name) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
+index 3091c174f7f8454035d015e96278e87284d5f399..bfa44c4e37618df3f745bccc6e775ce16c19490d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
+@@ -330,9 +330,12 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity {
+ container = CraftEventFactory.callInventoryOpenEvent(player, container);
+ if (container == null) return;
+
+- String title = container.getBukkitView().getTitle();
++ //String title = container.getBukkitView().getTitle(); // Paper - comment
++ net.kyori.adventure.text.Component adventure$title = container.getBukkitView().title(); // Paper
++ if (adventure$title == null) adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(container.getBukkitView().getTitle()); // Paper
+
+- player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0]));
++ //player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); // Paper - comment
++ player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, io.papermc.paper.adventure.PaperAdventure.asVanilla(adventure$title))); // Paper
+ player.containerMenu = container;
+ player.initMenu(container);
+ }
+@@ -402,8 +405,12 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity {
+
+ // Now open the window
+ MenuType<?> windowType = CraftContainer.getNotchInventoryType(inventory.getTopInventory());
+- String title = inventory.getTitle();
+- player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0]));
++
++ //String title = inventory.getTitle(); // Paper - comment
++ net.kyori.adventure.text.Component adventure$title = inventory.title(); // Paper
++ if (adventure$title == null) adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(inventory.getTitle()); // Paper
++ //player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, CraftChatMessage.fromString(title)[0])); // Paper - comment
++ player.connection.send(new ClientboundOpenScreenPacket(container.containerId, windowType, io.papermc.paper.adventure.PaperAdventure.asVanilla(adventure$title))); // Paper
+ player.containerMenu = container;
+ player.initMenu(container);
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java
+index 55945b83a5426b352bad9507cc9e94afb1278032..9ea1537408ff2d790747b6e5a681d9171a4233ae 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java
+@@ -59,6 +59,13 @@ public class CraftMinecartCommand extends CraftMinecart implements CommandMineca
+ return CraftChatMessage.fromComponent(this.getHandle().getCommandBlock().getName());
+ }
+
++ // Paper start
++ @Override
++ public [email protected] Component name() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getCommandBlock().getName());
++ }
++ // Paper end
++
+ @Override
+ public boolean isOp() {
+ return true;
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index e8ff50f1d799984b49116ef2dd1be70e3a655a10..6172bce93681e94b4cb19f7164f739e599108e00 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -395,14 +395,40 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public String getDisplayName() {
++ if(true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper
+ return this.getHandle().displayName;
+ }
+
+ @Override
+ public void setDisplayName(final String name) {
++ this.getHandle().adventure$displayName = name != null ? net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(name) : net.kyori.adventure.text.Component.text(this.getName()); // Paper
+ this.getHandle().displayName = name == null ? this.getName() : name;
+ }
+
++ // Paper start
++ @Override
++ public void playerListName(net.kyori.adventure.text.Component name) {
++ getHandle().listName = name == null ? null : io.papermc.paper.adventure.PaperAdventure.asVanilla(name);
++ if (getHandle().connection == null) return; // Updates are possible before the player has fully joined
++ for (ServerPlayer player : server.getHandle().players) {
++ if (player.getBukkitEntity().canSee(this)) {
++ player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, getHandle()));
++ }
++ }
++ }
++ @Override
++ public net.kyori.adventure.text.Component playerListName() {
++ return getHandle().listName == null ? net.kyori.adventure.text.Component.text(getName()) : io.papermc.paper.adventure.PaperAdventure.asAdventure(getHandle().listName);
++ }
++ @Override
++ public net.kyori.adventure.text.Component playerListHeader() {
++ return playerListHeader;
++ }
++ @Override
++ public net.kyori.adventure.text.Component playerListFooter() {
++ return playerListFooter;
++ }
++ // Paper end
+ @Override
+ public String getPlayerListName() {
+ return this.getHandle().listName == null ? this.getName() : CraftChatMessage.fromComponent(this.getHandle().listName);
+@@ -414,6 +440,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ name = this.getName();
+ }
+ this.getHandle().listName = name.equals(this.getName()) ? null : CraftChatMessage.fromStringOrNull(name);
++ if (this.getHandle().connection == null) return; // Paper - Updates are possible before the player has fully joined
+ for (ServerPlayer player : (List<ServerPlayer>) this.server.getHandle().players) {
+ if (player.getBukkitEntity().canSee(this)) {
+ player.connection.send(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, this.getHandle()));
+@@ -433,42 +460,42 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ this.getHandle().listOrder = order;
+ }
+
+- private Component playerListHeader;
+- private Component playerListFooter;
++ private net.kyori.adventure.text.Component playerListHeader; // Paper - Adventure
++ private net.kyori.adventure.text.Component playerListFooter; // Paper - Adventure
+
+ @Override
+ public String getPlayerListHeader() {
+- return (this.playerListHeader == null) ? null : CraftChatMessage.fromComponent(this.playerListHeader);
++ return (this.playerListHeader == null) ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.playerListHeader);
+ }
+
+ @Override
+ public String getPlayerListFooter() {
+- return (this.playerListFooter == null) ? null : CraftChatMessage.fromComponent(this.playerListFooter);
++ return (this.playerListFooter == null) ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.playerListFooter); // Paper - Adventure
+ }
+
+ @Override
+ public void setPlayerListHeader(String header) {
+- this.playerListHeader = CraftChatMessage.fromStringOrNull(header, true);
++ this.playerListHeader = header == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(header); // Paper - Adventure
+ this.updatePlayerListHeaderFooter();
+ }
+
+ @Override
+ public void setPlayerListFooter(String footer) {
+- this.playerListFooter = CraftChatMessage.fromStringOrNull(footer, true);
++ this.playerListFooter = footer == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(footer); // Paper - Adventure
+ this.updatePlayerListHeaderFooter();
+ }
+
+ @Override
+ public void setPlayerListHeaderFooter(String header, String footer) {
+- this.playerListHeader = CraftChatMessage.fromStringOrNull(header, true);
+- this.playerListFooter = CraftChatMessage.fromStringOrNull(footer, true);
++ this.playerListHeader = header == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(header); // Paper - Adventure
++ this.playerListFooter = footer == null ? null : net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(footer); // Paper - Adventure
+ this.updatePlayerListHeaderFooter();
+ }
+
+ private void updatePlayerListHeaderFooter() {
+ if (this.getHandle().connection == null) return;
+
+- ClientboundTabListPacket packet = new ClientboundTabListPacket((this.playerListHeader == null) ? Component.empty() : this.playerListHeader, (this.playerListFooter == null) ? Component.empty() : this.playerListFooter);
++ ClientboundTabListPacket packet = new ClientboundTabListPacket(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListHeader), io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListFooter)); // Paper - adventure
+ this.getHandle().connection.send(packet);
+ }
+
+@@ -498,6 +525,23 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ this.getHandle().transferCookieConnection.kickPlayer(CraftChatMessage.fromStringOrEmpty(message, true));
+ }
+
++ // Paper start
++ private static final net.kyori.adventure.text.Component DEFAULT_KICK_COMPONENT = net.kyori.adventure.text.Component.translatable("multiplayer.disconnect.kicked");
++ @Override
++ public void kick() {
++ this.kick(DEFAULT_KICK_COMPONENT);
++ }
++
++ @Override
++ public void kick(final net.kyori.adventure.text.Component message) {
++ org.spigotmc.AsyncCatcher.catchOp("player kick");
++ final ServerGamePacketListenerImpl connection = this.getHandle().connection;
++ if (connection != null) {
++ connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message);
++ }
++ }
++ // Paper end
++
+ @Override
+ public void setCompassTarget(Location loc) {
+ Preconditions.checkArgument(loc != null, "Location cannot be null");
+@@ -794,6 +838,24 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ this.getHandle().connection.send(packet);
+ }
+
++ // Paper start
++ @Override
++ public void sendSignChange(Location loc, @Nullable List<? extends net.kyori.adventure.text.Component> lines, DyeColor dyeColor, boolean hasGlowingText) {
++ if (getHandle().connection == null) {
++ return;
++ }
++ if (lines == null) {
++ lines = new java.util.ArrayList<>(4);
++ }
++ Preconditions.checkArgument(loc != null, "Location cannot be null");
++ Preconditions.checkArgument(dyeColor != null, "DyeColor cannot be null");
++ if (lines.size() < 4) {
++ throw new IllegalArgumentException("Must have at least 4 lines");
++ }
++ Component[] components = CraftSign.sanitizeLines(lines);
++ this.sendSignChange0(components, loc, dyeColor, hasGlowingText);
++ }
++ // Paper end
+ @Override
+ public void sendSignChange(Location loc, String[] lines) {
+ this.sendSignChange(loc, lines, DyeColor.BLACK);
+@@ -817,6 +879,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ if (this.getHandle().connection == null) return;
+
+ Component[] components = CraftSign.sanitizeLines(lines);
++ // Paper start - adventure
++ this.sendSignChange0(components, loc, dyeColor, hasGlowingText);
++ }
++
++ private void sendSignChange0(Component[] components, Location loc, DyeColor dyeColor, boolean hasGlowingText) {
++ // Paper end
+ SignBlockEntity sign = new SignBlockEntity(CraftLocation.toBlockPosition(loc), Blocks.OAK_SIGN.defaultBlockState());
+ SignText text = sign.getFrontText();
+ text = text.setColor(net.minecraft.world.item.DyeColor.byId(dyeColor.getWoolData()));
+@@ -1843,7 +1911,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public void setResourcePack(String url) {
+- this.setResourcePack(url, null);
++ this.setResourcePack(url, (byte[]) null);
+ }
+
+ @Override
+@@ -1858,7 +1926,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public void setResourcePack(String url, byte[] hash, boolean force) {
+- this.setResourcePack(url, hash, null, force);
++ this.setResourcePack(url, hash, (String) null, force);
+ }
+
+ @Override
+@@ -1895,6 +1963,59 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ this.handlePushResourcePack(new ClientboundResourcePackPushPacket(id, url, hashStr, force, CraftChatMessage.fromStringOrOptional(prompt, true)), false);
+ }
+
++ // Paper start - adventure
++ @Override
++ 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) {
++ Preconditions.checkArgument(hashBytes.length == 20, "Resource pack hash should be 20 bytes long but was " + hashBytes.length);
++ hash = BaseEncoding.base16().lowerCase().encode(hashBytes);
++ } else {
++ hash = "";
++ }
++ this.getHandle().connection.send(new ClientboundResourcePackPopPacket(Optional.empty()));
++ this.getHandle().connection.send(new ClientboundResourcePackPushPacket(uuid, url, hash, force, Optional.ofNullable(prompt).map(io.papermc.paper.adventure.PaperAdventure::asVanilla)));
++ }
++
++ @SuppressWarnings({"unchecked", "rawtypes"})
++ void sendBundle(final List<? extends net.minecraft.network.protocol.Packet<? extends net.minecraft.network.protocol.common.ClientCommonPacketListener>> packet) {
++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket((List) packet));
++ }
++
++ @Override
++ public void sendResourcePacks(final net.kyori.adventure.resource.ResourcePackRequest request) {
++ if (this.getHandle().connection == null) return;
++ final List<ClientboundResourcePackPushPacket> packs = new java.util.ArrayList<>(request.packs().size());
++ if (request.replace()) {
++ this.clearResourcePacks();
++ }
++ final Component prompt = io.papermc.paper.adventure.PaperAdventure.asVanilla(request.prompt());
++ for (final java.util.Iterator<net.kyori.adventure.resource.ResourcePackInfo> iter = request.packs().iterator(); iter.hasNext();) {
++ final net.kyori.adventure.resource.ResourcePackInfo pack = iter.next();
++ packs.add(new ClientboundResourcePackPushPacket(pack.id(), pack.uri().toASCIIString(), pack.hash(), request.required(), iter.hasNext() ? Optional.empty() : Optional.ofNullable(prompt)));
++ if (request.callback() != net.kyori.adventure.resource.ResourcePackCallback.noOp()) {
++ this.getHandle().connection.packCallbacks.put(pack.id(), request.callback()); // just override if there is a previously existing callback
++ }
++ }
++ this.sendBundle(packs);
++ super.sendResourcePacks(request);
++ }
++
++ @Override
++ public void removeResourcePacks(final UUID id, final UUID ... others) {
++ if (this.getHandle().connection == null) return;
++ this.sendBundle(net.kyori.adventure.util.MonkeyBars.nonEmptyArrayToList(pack -> new ClientboundResourcePackPopPacket(Optional.of(pack)), id, others));
++ }
++
++ @Override
++ public void clearResourcePacks() {
++ if (this.getHandle().connection == null) return;
++ this.getHandle().connection.send(new ClientboundResourcePackPopPacket(Optional.empty()));
++ }
++ // Paper end - adventure
++
+ @Override
+ public void removeResourcePack(UUID id) {
+ Preconditions.checkArgument(id != null, "Resource pack id cannot be null");
+@@ -2300,6 +2421,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ return (this.getHandle().requestedViewDistance() == 0) ? Bukkit.getViewDistance() : this.getHandle().requestedViewDistance();
+ }
+
++ // Paper start
++ @Override
++ public java.util.Locale locale() {
++ return getHandle().adventure$locale;
++ }
++ // Paper end
+ @Override
+ public int getPing() {
+ return this.getHandle().connection.latency();
+@@ -2350,6 +2477,248 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ return this.getHandle().allowsListing();
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component displayName() {
++ return this.getHandle().adventure$displayName;
++ }
++
++ @Override
++ public void displayName(final net.kyori.adventure.text.Component displayName) {
++ this.getHandle().adventure$displayName = displayName != null ? displayName : net.kyori.adventure.text.Component.text(this.getName());
++ this.getHandle().displayName = null;
++ }
++
++ @Override
++ public void deleteMessage(net.kyori.adventure.chat.SignedMessage.Signature signature) {
++ if (getHandle().connection == null) return;
++ net.minecraft.network.chat.MessageSignature sig = new net.minecraft.network.chat.MessageSignature(signature.bytes());
++
++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundDeleteChatPacket(new net.minecraft.network.chat.MessageSignature.Packed(sig)));
++ }
++
++ private net.minecraft.network.chat.ChatType.Bound toHandle(net.kyori.adventure.chat.ChatType.Bound boundChatType) {
++ net.minecraft.core.Registry<net.minecraft.network.chat.ChatType> chatTypeRegistry = this.getHandle().level().registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.CHAT_TYPE);
++
++ return new net.minecraft.network.chat.ChatType.Bound(
++ chatTypeRegistry.getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.CHAT_TYPE, io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.type().key()))),
++ io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.name()),
++ Optional.ofNullable(io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.target()))
++ );
++ }
++
++ @Override
++ public void sendMessage(net.kyori.adventure.text.Component message, net.kyori.adventure.chat.ChatType.Bound boundChatType) {
++ if (getHandle().connection == null) return;
++
++ net.minecraft.network.chat.Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(message);
++ this.getHandle().sendChatMessage(new net.minecraft.network.chat.OutgoingChatMessage.Disguised(component), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType));
++ }
++
++ @Override
++ public void sendMessage(net.kyori.adventure.chat.SignedMessage signedMessage, net.kyori.adventure.chat.ChatType.Bound boundChatType) {
++ if (getHandle().connection == null) return;
++
++ if (signedMessage instanceof PlayerChatMessage.AdventureView view) {
++ this.getHandle().sendChatMessage(net.minecraft.network.chat.OutgoingChatMessage.create(view.playerChatMessage()), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType));
++ return;
++ }
++ net.kyori.adventure.text.Component message = signedMessage.unsignedContent() == null ? net.kyori.adventure.text.Component.text(signedMessage.message()) : signedMessage.unsignedContent();
++ if (signedMessage.isSystem()) {
++ this.sendMessage(message, boundChatType);
++ } else {
++ super.sendMessage(signedMessage, boundChatType);
++ }
++// net.minecraft.network.chat.PlayerChatMessage playerChatMessage = new net.minecraft.network.chat.PlayerChatMessage(
++// null, // TODO:
++// new net.minecraft.network.chat.MessageSignature(signedMessage.signature().bytes()),
++// null, // TODO
++// io.papermc.paper.adventure.PaperAdventure.asVanilla(signedMessage.unsignedContent()),
++// net.minecraft.network.chat.FilterMask.PASS_THROUGH
++// );
++//
++// this.getHandle().sendChatMessage(net.minecraft.network.chat.OutgoingChatMessage.create(playerChatMessage), this.getHandle().isTextFilteringEnabled(), this.toHandle(boundChatType));
++ }
++
++ @Deprecated(forRemoval = true)
++ @Override
++ public void sendMessage(final net.kyori.adventure.identity.Identity identity, final net.kyori.adventure.text.Component message, final net.kyori.adventure.audience.MessageType type) {
++ if (getHandle().connection == null) return;
++ final net.minecraft.core.Registry<net.minecraft.network.chat.ChatType> chatTypeRegistry = this.getHandle().level().registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.CHAT_TYPE);
++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(message, false));
++ }
++
++ @Override
++ public void sendActionBar(final net.kyori.adventure.text.Component message) {
++ final net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket packet = new net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(message));
++ this.getHandle().connection.send(packet);
++ }
++
++ @Override
++ public void sendPlayerListHeader(final net.kyori.adventure.text.Component header) {
++ this.playerListHeader = header;
++ this.adventure$sendPlayerListHeaderAndFooter();
++ }
++
++ @Override
++ public void sendPlayerListFooter(final net.kyori.adventure.text.Component footer) {
++ this.playerListFooter = footer;
++ this.adventure$sendPlayerListHeaderAndFooter();
++ }
++
++ @Override
++ public void sendPlayerListHeaderAndFooter(final net.kyori.adventure.text.Component header, final net.kyori.adventure.text.Component footer) {
++ this.playerListHeader = header;
++ this.playerListFooter = footer;
++ this.adventure$sendPlayerListHeaderAndFooter();
++ }
++
++ private void adventure$sendPlayerListHeaderAndFooter() {
++ final ServerGamePacketListenerImpl connection = this.getHandle().connection;
++ if (connection == null) return;
++ final ClientboundTabListPacket packet = new ClientboundTabListPacket(
++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListHeader),
++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(this.playerListFooter)
++ );
++ connection.send(packet);
++ }
++
++ @Override
++ public void showTitle(final net.kyori.adventure.title.Title title) {
++ final ServerGamePacketListenerImpl connection = this.getHandle().connection;
++ final net.kyori.adventure.title.Title.Times times = title.times();
++ if (times != null) {
++ connection.send(new ClientboundSetTitlesAnimationPacket(ticks(times.fadeIn()), ticks(times.stay()), ticks(times.fadeOut())));
++ }
++ final ClientboundSetSubtitleTextPacket sp = new ClientboundSetSubtitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(title.subtitle()));
++ connection.send(sp);
++ final ClientboundSetTitleTextPacket tp = new ClientboundSetTitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(title.title()));
++ connection.send(tp);
++ }
++
++ @Override
++ public <T> void sendTitlePart(final net.kyori.adventure.title.TitlePart<T> part, T value) {
++ java.util.Objects.requireNonNull(part, "part");
++ java.util.Objects.requireNonNull(value, "value");
++ if (part == net.kyori.adventure.title.TitlePart.TITLE) {
++ final ClientboundSetTitleTextPacket tp = new ClientboundSetTitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla((net.kyori.adventure.text.Component)value));
++ this.getHandle().connection.send(tp);
++ } else if (part == net.kyori.adventure.title.TitlePart.SUBTITLE) {
++ final ClientboundSetSubtitleTextPacket sp = new ClientboundSetSubtitleTextPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla((net.kyori.adventure.text.Component)value));
++ this.getHandle().connection.send(sp);
++ } else if (part == net.kyori.adventure.title.TitlePart.TIMES) {
++ final net.kyori.adventure.title.Title.Times times = (net.kyori.adventure.title.Title.Times) value;
++ this.getHandle().connection.send(new ClientboundSetTitlesAnimationPacket(ticks(times.fadeIn()), ticks(times.stay()), ticks(times.fadeOut())));
++ } else {
++ throw new IllegalArgumentException("Unknown TitlePart");
++ }
++ }
++
++ private static int ticks(final java.time.Duration duration) {
++ if (duration == null) {
++ return -1;
++ }
++ return (int) (duration.toMillis() / 50L);
++ }
++
++ @Override
++ public void clearTitle() {
++ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundClearTitlesPacket(false));
++ }
++
++ // resetTitle implemented above
++
++ private @Nullable Set<net.kyori.adventure.bossbar.BossBar> activeBossBars;
++
++ @Override
++ public @NotNull Iterable<? extends net.kyori.adventure.bossbar.BossBar> activeBossBars() {
++ if (this.activeBossBars != null) {
++ return java.util.Collections.unmodifiableSet(this.activeBossBars);
++ }
++ return Set.of();
++ }
++
++ @Override
++ public void showBossBar(final net.kyori.adventure.bossbar.BossBar bar) {
++ net.kyori.adventure.bossbar.BossBarImplementation.get(bar, io.papermc.paper.adventure.BossBarImplementationImpl.class).playerShow(this);
++ if (this.activeBossBars == null) {
++ this.activeBossBars = new HashSet<>();
++ }
++ this.activeBossBars.add(bar);
++ }
++
++ @Override
++ public void hideBossBar(final net.kyori.adventure.bossbar.BossBar bar) {
++ net.kyori.adventure.bossbar.BossBarImplementation.get(bar, io.papermc.paper.adventure.BossBarImplementationImpl.class).playerHide(this);
++ if (this.activeBossBars != null) {
++ this.activeBossBars.remove(bar);
++ if (this.activeBossBars.isEmpty()) {
++ this.activeBossBars = null;
++ }
++ }
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound) {
++ final net.minecraft.world.phys.Vec3 pos = this.getHandle().position();
++ this.playSound(sound, pos.x, pos.y, pos.z);
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final double x, final double y, final double z) {
++ this.getHandle().connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, x, y, z, sound.seed().orElseGet(this.getHandle().getRandom()::nextLong), null));
++ }
++
++ @Override
++ public void playSound(final net.kyori.adventure.sound.Sound sound, final net.kyori.adventure.sound.Sound.Emitter emitter) {
++ final Entity entity;
++ if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) {
++ entity = this.getHandle();
++ } else if (emitter instanceof CraftEntity craftEntity) {
++ entity = craftEntity.getHandle();
++ } else {
++ throw new IllegalArgumentException("Sound emitter must be an Entity or self(), but was: " + emitter);
++ }
++ this.getHandle().connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, entity, sound.seed().orElseGet(this.getHandle().getRandom()::nextLong), null));
++ }
++
++ @Override
++ public void stopSound(final net.kyori.adventure.sound.SoundStop stop) {
++ this.getHandle().connection.send(new ClientboundStopSoundPacket(
++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullable(stop.sound()),
++ io.papermc.paper.adventure.PaperAdventure.asVanillaNullable(stop.source())
++ ));
++ }
++
++ @Override
++ public void openBook(final net.kyori.adventure.inventory.Book book) {
++ final java.util.Locale locale = this.getHandle().adventure$locale;
++ final net.minecraft.world.item.ItemStack item = io.papermc.paper.adventure.PaperAdventure.asItemStack(book, locale);
++ final ServerPlayer player = this.getHandle();
++ final ServerGamePacketListenerImpl connection = player.connection;
++ final net.minecraft.world.entity.player.Inventory inventory = player.getInventory();
++ final int slot = inventory.items.size() + inventory.selected;
++ final int stateId = getHandle().containerMenu.getStateId();
++ connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, item));
++ connection.send(new net.minecraft.network.protocol.game.ClientboundOpenBookPacket(net.minecraft.world.InteractionHand.MAIN_HAND));
++ connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(0, stateId, slot, inventory.getSelected()));
++ }
++
++ @Override
++ public net.kyori.adventure.pointer.Pointers pointers() {
++ if (this.adventure$pointers == null) {
++ this.adventure$pointers = net.kyori.adventure.pointer.Pointers.builder()
++ .withDynamic(net.kyori.adventure.identity.Identity.DISPLAY_NAME, this::displayName)
++ .withDynamic(net.kyori.adventure.identity.Identity.NAME, this::getName)
++ .withDynamic(net.kyori.adventure.identity.Identity.UUID, this::getUniqueId)
++ .withStatic(net.kyori.adventure.permission.PermissionChecker.POINTER, this::permissionValue)
++ .withDynamic(net.kyori.adventure.identity.Identity.LOCALE, this::locale)
++ .build();
++ }
++
++ return this.adventure$pointers;
++ }
++ // Paper end
+ // Spigot start
+ private final Player.Spigot spigot = new Player.Spigot()
+ {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java
+index 5725b0281ac53a2354b233223259d6784353bc6e..9ef939b76d06874b856e0c850addb364146f5a00 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java
+@@ -32,6 +32,17 @@ public class CraftTextDisplay extends CraftDisplay implements TextDisplay {
+ public void setText(String text) {
+ this.getHandle().setText(CraftChatMessage.fromString(text, true)[0]);
+ }
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component text() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getHandle().getText());
++ }
++
++ @Override
++ public void text(net.kyori.adventure.text.Component text) {
++ this.getHandle().setText(text == null ? net.minecraft.network.chat.Component.empty() : io.papermc.paper.adventure.PaperAdventure.asVanilla(text));
++ }
++ // Paper end
+
+ @Override
+ public int getLineWidth() {
+diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+index b5034787283b5a0403c11281895563372fccb5d2..8ea4d63833cd1500d7f413f761aa9a7cf26520c0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+@@ -915,7 +915,7 @@ public class CraftEventFactory {
+ return event;
+ }
+
+- public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, DamageSource damageSource, List<org.bukkit.inventory.ItemStack> drops, String deathMessage, boolean keepInventory) {
++ public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, DamageSource damageSource, List<org.bukkit.inventory.ItemStack> drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure
+ CraftPlayer entity = victim.getBukkitEntity();
+ CraftDamageSource bukkitDamageSource = new CraftDamageSource(damageSource);
+ PlayerDeathEvent event = new PlayerDeathEvent(entity, bukkitDamageSource, drops, victim.getExpReward(victim.serverLevel(), damageSource.getEntity()), 0, deathMessage);
+@@ -948,7 +948,7 @@ public class CraftEventFactory {
+ * Server methods
+ */
+ public static ServerListPingEvent callServerListPingEvent(SocketAddress address, String motd, int numPlayers, int maxPlayers) {
+- ServerListPingEvent event = new ServerListPingEvent("", ((InetSocketAddress) address).getAddress(), motd, numPlayers, maxPlayers);
++ ServerListPingEvent event = new ServerListPingEvent("", ((InetSocketAddress) address).getAddress(), Bukkit.getServer().motd(), numPlayers, maxPlayers);
+ Bukkit.getServer().getPluginManager().callEvent(event);
+ return event;
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
+index 45a8d918d9ecf459a4e66dca555a022ceb507222..1a2329021a6b29777c637ee4dc8cd69ed18001c9 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
+@@ -72,6 +72,13 @@ public class CraftContainer extends AbstractContainerMenu {
+ return inventory.getType();
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component title() {
++ return inventory instanceof CraftInventoryCustom ? ((CraftInventoryCustom.MinecraftInventory) ((CraftInventory) inventory).getInventory()).title() : net.kyori.adventure.text.Component.text(inventory.getType().getDefaultTitle());
++ }
++ // Paper end
++
+ @Override
+ public String getTitle() {
+ return this.title;
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
+index c9cc23757a9fcc58d30b2915d4c5cfbc7d1c767a..fc0e1212022d1aa3506699b60ef338196eb54eba 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
+@@ -19,6 +19,12 @@ public class CraftInventoryCustom extends CraftInventory {
+ super(new MinecraftInventory(owner, type));
+ }
+
++ // Paper start
++ public CraftInventoryCustom(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ super(new MinecraftInventory(owner, type, title));
++ }
++ // Paper end
++
+ public CraftInventoryCustom(InventoryHolder owner, InventoryType type, String title) {
+ super(new MinecraftInventory(owner, type, title));
+ }
+@@ -27,6 +33,12 @@ public class CraftInventoryCustom extends CraftInventory {
+ super(new MinecraftInventory(owner, size));
+ }
+
++ // Paper start
++ public CraftInventoryCustom(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) {
++ super(new MinecraftInventory(owner, size, title));
++ }
++ // Paper end
++
+ public CraftInventoryCustom(InventoryHolder owner, int size, String title) {
+ super(new MinecraftInventory(owner, size, title));
+ }
+@@ -36,9 +48,17 @@ public class CraftInventoryCustom extends CraftInventory {
+ private int maxStack = MAX_STACK;
+ private final List<HumanEntity> viewers;
+ private final String title;
++ private final net.kyori.adventure.text.Component adventure$title; // Paper
+ private InventoryType type;
+ private final InventoryHolder owner;
+
++ // Paper start
++ public MinecraftInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ this(owner, type.getDefaultSize(), title);
++ this.type = type;
++ }
++ // Paper end
++
+ public MinecraftInventory(InventoryHolder owner, InventoryType type) {
+ this(owner, type.getDefaultSize(), type.getDefaultTitle());
+ this.type = type;
+@@ -57,11 +77,24 @@ public class CraftInventoryCustom extends CraftInventory {
+ Preconditions.checkArgument(title != null, "title cannot be null");
+ this.items = NonNullList.withSize(size, ItemStack.EMPTY);
+ this.title = title;
++ this.adventure$title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title);
+ this.viewers = new ArrayList<HumanEntity>();
+ this.owner = owner;
+ this.type = InventoryType.CHEST;
+ }
+
++ // Paper start
++ public MinecraftInventory(final InventoryHolder owner, final int size, final net.kyori.adventure.text.Component title) {
++ Preconditions.checkArgument(title != null, "Title cannot be null");
++ this.items = NonNullList.withSize(size, ItemStack.EMPTY);
++ this.title = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(title);
++ this.adventure$title = title;
++ this.viewers = new ArrayList<HumanEntity>();
++ this.owner = owner;
++ this.type = InventoryType.CHEST;
++ }
++ // Paper end
++
+ @Override
+ public int getContainerSize() {
+ return this.items.size();
+@@ -183,6 +216,12 @@ public class CraftInventoryCustom extends CraftInventory {
+ return null;
+ }
+
++ // Paper start
++ public net.kyori.adventure.text.Component title() {
++ return this.adventure$title;
++ }
++ // Paper end
++
+ public String getTitle() {
+ return this.title;
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java
+index d89b178dc82c7e2ad6d586217c5a039688563e29..d674289b07748022b94cc6a7e6c6eb456d245c93 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryView.java
+@@ -73,6 +73,13 @@ public class CraftInventoryView<T extends AbstractContainerMenu, I extends Inven
+ return CraftItemStack.asCraftMirror(this.container.getSlot(slot).getItem());
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component title() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.container.getTitle());
++ }
++ // Paper end
++
+ @Override
+ public String getTitle() {
+ return this.title;
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java
+index cc70796e0a80d4c8ed4e0d448c583f6647d7d72c..2f57af25e5cdeb2295675309d4cb7f36d15256c5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java
+@@ -211,4 +211,21 @@ public final class CraftItemFactory implements ItemFactory {
+ Optional<HolderSet.Named<Enchantment>> optional = (allowTreasures) ? Optional.empty() : registry.lookupOrThrow(Registries.ENCHANTMENT).get(EnchantmentTags.IN_ENCHANTING_TABLE);
+ return CraftItemStack.asCraftMirror(EnchantmentHelper.enchantItem(source, craft.handle, level, registry, optional));
+ }
++
++ // Paper start - Adventure
++ @Override
++ public net.kyori.adventure.text.event.HoverEvent<net.kyori.adventure.text.event.HoverEvent.ShowItem> asHoverEvent(final ItemStack item, final java.util.function.UnaryOperator<net.kyori.adventure.text.event.HoverEvent.ShowItem> op) {
++ return net.kyori.adventure.text.event.HoverEvent.showItem(op.apply(
++ net.kyori.adventure.text.event.HoverEvent.ShowItem.showItem(
++ item.getType().getKey(),
++ item.getAmount(),
++ io.papermc.paper.adventure.PaperAdventure.asAdventure(CraftItemStack.unwrap(item).getComponentsPatch())) // unwrap is fine here because the components patch will be safely copied
++ ));
++ }
++
++ @Override
++ public [email protected] Component displayName(@org.jetbrains.annotations.NotNull ItemStack itemStack) {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(CraftItemStack.asNMSCopy(itemStack).getDisplayName());
++ }
++ // Paper end - Adventure
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
+index b09f794abd68551058e5764749d76c9ce8d2b849..d658634ea4468c9dbfb29bc12282441c96358566 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
+@@ -37,6 +37,12 @@ public class CraftMenuType<V extends InventoryView> implements MenuType.Typed<V>
+
+ @Override
+ public V create(final HumanEntity player, final String title) {
++ // Paper start - adventure
++ return create(player, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title));
++ }
++ @Override
++ public V create(final HumanEntity player, final net.kyori.adventure.text.Component title) {
++ // Paper end - adventure
+ Preconditions.checkArgument(player != null, "The given player must not be null");
+ Preconditions.checkArgument(title != null, "The given title must not be null");
+ Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
+@@ -45,7 +51,7 @@ public class CraftMenuType<V extends InventoryView> implements MenuType.Typed<V>
+ final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
+
+ final AbstractContainerMenu container = this.typeData.get().menuBuilder().build(serverPlayer, this.handle);
+- container.setTitle(CraftChatMessage.fromString(title)[0]);
++ container.setTitle(io.papermc.paper.adventure.PaperAdventure.asVanilla(title)); // Paper - adventure
+ container.checkReachable = false;
+ return (V) container.getBukkitView();
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
+index 13f8689b8c6a2f3c81325d5692dc25abf2121d74..06b3004fa4f3e89d6eb19d545afe548bfd565e06 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
+@@ -15,10 +15,17 @@ public class CraftMerchantCustom implements CraftMerchant {
+
+ private MinecraftMerchant merchant;
+
++ @Deprecated // Paper - Adventure
+ public CraftMerchantCustom(String title) {
+ this.merchant = new MinecraftMerchant(title);
+ this.getMerchant().craftMerchant = this;
+ }
++ // Paper start
++ public CraftMerchantCustom(net.kyori.adventure.text.Component title) {
++ this.merchant = new MinecraftMerchant(title);
++ getMerchant().craftMerchant = this;
++ }
++ // Paper end
+
+ @Override
+ public String toString() {
+@@ -37,10 +44,17 @@ public class CraftMerchantCustom implements CraftMerchant {
+ private Player tradingPlayer;
+ protected CraftMerchant craftMerchant;
+
++ @Deprecated // Paper - Adventure
+ public MinecraftMerchant(String title) {
+ Preconditions.checkArgument(title != null, "Title cannot be null");
+ this.title = CraftChatMessage.fromString(title)[0];
+ }
++ // Paper start
++ public MinecraftMerchant(net.kyori.adventure.text.Component title) {
++ Preconditions.checkArgument(title != null, "Title cannot be null");
++ this.title = io.papermc.paper.adventure.PaperAdventure.asVanilla(title);
++ }
++ // Paper end
+
+ @Override
+ public CraftMerchant getCraftMerchant() {
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java
+index 3ab6b212001c2b92cac42c0ff97e59c3d08b3e49..32e5188442551b3e72e1d4826d836d622d0e438a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java
+@@ -2,8 +2,9 @@ package org.bukkit.craftbukkit.inventory;
+
+ import com.google.common.base.Preconditions;
+ import com.google.common.collect.ImmutableList;
+-import com.google.common.collect.ImmutableMap.Builder;
+ import com.google.common.collect.Lists;
++
++import com.google.common.collect.ImmutableMap; // Paper
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.List;
+@@ -170,6 +171,128 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo
+ public void setGeneration(Generation generation) {
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component title() {
++ return null;
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta title(net.kyori.adventure.text.Component title) {
++ return this;
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component author() {
++ return null;
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta author(net.kyori.adventure.text.Component author) {
++ return this;
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component page(final int page) {
++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s/%s)", page, this.getPageCount());
++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.pages.get(page - 1));
++ }
++
++ @Override
++ public void page(final int page, net.kyori.adventure.text.Component data) {
++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s/%s)", page, this.getPageCount());
++ if (data == null) {
++ data = net.kyori.adventure.text.Component.empty();
++ }
++ this.pages.set(page - 1, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(data));
++ }
++
++ @Override
++ public List<net.kyori.adventure.text.Component> pages() {
++ if (this.pages == null) return ImmutableList.of();
++ return this.pages.stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::deserialize).collect(ImmutableList.toImmutableList());
++ }
++
++ @Override
++ public BookMeta pages(List<net.kyori.adventure.text.Component> pages) {
++ if (this.pages != null) this.pages.clear();
++ for (net.kyori.adventure.text.Component page : pages) {
++ this.addPages(page);
++ }
++ return this;
++ }
++
++ @Override
++ public BookMeta pages(net.kyori.adventure.text.Component... pages) {
++ if (this.pages != null) this.pages.clear();
++ this.addPages(pages);
++ return this;
++ }
++
++ @Override
++ public void addPages(net.kyori.adventure.text.Component... pages) {
++ if (this.pages == null) this.pages = new ArrayList<>();
++ for (net.kyori.adventure.text.Component page : pages) {
++ if (this.pages.size() >= MAX_PAGES) {
++ return;
++ }
++
++ if (page == null) {
++ page = net.kyori.adventure.text.Component.empty();
++ }
++
++ this.pages.add(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(page));
++ }
++ }
++
++ private CraftMetaBook(List<net.kyori.adventure.text.Component> pages) {
++ super((org.bukkit.craftbukkit.inventory.CraftMetaItem) org.bukkit.Bukkit.getItemFactory().getItemMeta(org.bukkit.Material.WRITABLE_BOOK));
++ this.pages = pages.subList(0, Math.min(MAX_PAGES, pages.size())).stream().map(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()::serialize).collect(java.util.stream.Collectors.toList());
++ }
++
++ static class CraftMetaBookBuilder implements BookMetaBuilder {
++ protected final List<net.kyori.adventure.text.Component> pages = new java.util.ArrayList<>();
++
++ @Override
++ public BookMetaBuilder title(net.kyori.adventure.text.Component title) {
++ return this;
++ }
++
++ @Override
++ public BookMetaBuilder author(net.kyori.adventure.text.Component author) {
++ return this;
++ }
++
++ @Override
++ public BookMetaBuilder addPage(net.kyori.adventure.text.Component page) {
++ this.pages.add(page);
++ return this;
++ }
++
++ @Override
++ public BookMetaBuilder pages(net.kyori.adventure.text.Component... pages) {
++ java.util.Collections.addAll(this.pages, pages);
++ return this;
++ }
++
++ @Override
++ public BookMetaBuilder pages(java.util.Collection<net.kyori.adventure.text.Component> pages) {
++ this.pages.addAll(pages);
++ return this;
++ }
++
++ @Override
++ public BookMeta build() {
++ return new CraftMetaBook(this.pages);
++ }
++ }
++
++ @Override
++ public BookMetaBuilder toBuilder() {
++ return new CraftMetaBookBuilder();
++ }
++
++ // Paper end
+ @Override
+ public String getPage(final int page) {
+ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s)", page);
+@@ -286,7 +409,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta, WritableBo
+ }
+
+ @Override
+- Builder<String, Object> serialize(Builder<String, Object> builder) {
++ ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> builder) {
+ super.serialize(builder);
+
+ if (this.pages != null) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java
+index c71a4971f127fdfc753306019313ce1a31201120..fd3b12477c30d1eabdbe57ea779027931e9dd957 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java
+@@ -346,7 +346,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta {
+ }
+
+ @Override
+- Builder<String, Object> serialize(Builder<String, Object> builder) {
++ com.google.common.collect.ImmutableMap.Builder<String, Object> serialize(com.google.common.collect.ImmutableMap.Builder<String, Object> builder) { // Paper - adventure - fqn as it conflicts with adventure book builder
+ super.serialize(builder);
+
+ if (this.hasTitle()) {
+@@ -459,4 +459,111 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta {
+ return this.spigot;
+ }
+ // Spigot end
++
++ // Paper start - adventure
++ public static final net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.builder()
++ .character(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.SECTION_CHAR)
++ .build();
++ private CraftMetaBookSigned(net.kyori.adventure.text.Component title, net.kyori.adventure.text.Component author, java.util.List<net.kyori.adventure.text.Component> pages) {
++ super((org.bukkit.craftbukkit.inventory.CraftMetaItem) org.bukkit.Bukkit.getItemFactory().getItemMeta(Material.WRITABLE_BOOK));
++ this.title = title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(title);
++ this.author = author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(author);
++ this.pages = io.papermc.paper.adventure.PaperAdventure.asVanilla(pages.subList(0, Math.min(MAX_PAGES, pages.size())));
++ }
++
++ static final class CraftMetaBookSignedBuilder extends CraftMetaBook.CraftMetaBookBuilder {
++ private net.kyori.adventure.text.Component title;
++ private net.kyori.adventure.text.Component author;
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta.BookMetaBuilder title(final net.kyori.adventure.text.Component title) {
++ this.title = title;
++ return this;
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta.BookMetaBuilder author(final net.kyori.adventure.text.Component author) {
++ this.author = author;
++ return this;
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta build() {
++ return new CraftMetaBookSigned(this.title, this.author, this.pages);
++ }
++ }
++
++ @Override
++ public BookMetaBuilder toBuilder() {
++ return new CraftMetaBookSignedBuilder();
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component title() {
++ return this.title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.deserialize(this.title);
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta title(net.kyori.adventure.text.Component title) {
++ this.setTitle(title == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(title));
++ return this;
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component author() {
++ return this.author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.deserialize(this.author);
++ }
++
++ @Override
++ public org.bukkit.inventory.meta.BookMeta author(net.kyori.adventure.text.Component author) {
++ this.setAuthor(author == null ? null : LEGACY_DOWNSAMPLING_COMPONENT_SERIALIZER.serialize(author));
++ return this;
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component page(final int page) {
++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s/%s)", page, this.getPageCount());
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.pages.get(page - 1));
++ }
++
++ @Override
++ public void page(final int page, net.kyori.adventure.text.Component data) {
++ Preconditions.checkArgument(this.isValidPage(page), "Invalid page number (%s/%s)", page, this.getPageCount());
++ this.pages.set(page - 1, io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(data));
++ }
++
++ @Override
++ public List<net.kyori.adventure.text.Component> pages() {
++ if (this.pages == null) return ImmutableList.of();
++ return this.pages.stream().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).collect(ImmutableList.toImmutableList());
++ }
++
++ @Override
++ public BookMeta pages(List<net.kyori.adventure.text.Component> pages) {
++ if (this.pages != null) this.pages.clear();
++ for (net.kyori.adventure.text.Component page : pages) {
++ this.addPages(page);
++ }
++ return this;
++ }
++
++ @Override
++ public BookMeta pages(net.kyori.adventure.text.Component... pages) {
++ if (this.pages != null) this.pages.clear();
++ this.addPages(pages);
++ return this;
++ }
++
++ @Override
++ public void addPages(net.kyori.adventure.text.Component... pages) {
++ if (this.pages == null) this.pages = new ArrayList<>();
++ for (net.kyori.adventure.text.Component page : pages) {
++ if (this.pages.size() >= MAX_PAGES) {
++ return;
++ }
++
++ this.pages.add(io.papermc.paper.adventure.PaperAdventure.asVanillaNullToEmpty(page));
++ }
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+index 92dcf22ee3b9cceb742b77c4cc58645eb25d9e67..aa14b5c363824761e81a9a29ae88820841df0166 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+@@ -1103,6 +1103,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return !(this.hasDisplayName() || this.hasItemName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasEnchantable() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.build().isEmpty() || !this.removedTags.isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isHideTooltip() || this.hasTooltipStyle() || this.hasItemModel() || this.isUnbreakable() || this.hasEnchantmentGlintOverride() || this.isGlider() || this.hasDamageResistant() || this.hasMaxStackSize() || this.hasRarity() || this.hasUseRemainder() || this.hasUseCooldown() || this.hasFood() || this.hasTool() || this.hasJukeboxPlayable() || this.hasEquippable() || this.hasDamage() || this.hasMaxDamage() || this.hasAttributeModifiers() || this.customTag != null);
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component displayName() {
++ return displayName == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(displayName);
++ }
++
++ @Override
++ public void displayName(final net.kyori.adventure.text.Component displayName) {
++ this.displayName = displayName == null ? null : io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName);
++ }
++ // Paper end
++
+ @Override
+ public String getDisplayName() {
+ return CraftChatMessage.fromComponent(this.displayName);
+@@ -1133,6 +1145,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return this.itemName != null;
+ }
+
++ // Paper start - Adventure
++ @Override
++ public net.kyori.adventure.text.Component itemName() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.itemName);
++ }
++
++ @Override
++ public void itemName(final net.kyori.adventure.text.Component name) {
++ this.itemName = io.papermc.paper.adventure.PaperAdventure.asVanilla(name);
++ }
++ // Paper end - Adventure
++
+ @Override
+ public String getLocalizedName() {
+ return this.getDisplayName();
+@@ -1152,6 +1176,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return this.lore != null && !this.lore.isEmpty();
+ }
+
++ // Paper start
++ @Override
++ public List<net.kyori.adventure.text.Component> lore() {
++ return this.lore != null ? io.papermc.paper.adventure.PaperAdventure.asAdventure(this.lore) : null;
++ }
++
++ @Override
++ public void lore(final List<? extends net.kyori.adventure.text.Component> lore) {
++ this.lore = lore != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(lore) : null;
++ }
++ // Paper end
++
+ @Override
+ public boolean hasRepairCost() {
+ return this.repairCost > 0;
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
+index 38fb47bbfcec739be795b46cfb7c2c41a8379fea..caf7e4312e95e90dd0822355c8832006e69a2700 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
+@@ -60,6 +60,14 @@ public class CraftTrimMaterial implements TrimMaterial, Handleable<net.minecraft
+ @NotNull
+ @Override
+ public String getTranslationKey() {
++ if (!(this.handle.description().getContents() instanceof TranslatableContents)) throw new UnsupportedOperationException("Description isn't translatable!"); // Paper
+ return ((TranslatableContents) this.handle.description().getContents()).getKey();
+ }
++
++ // Paper start - adventure
++ @Override
++ public net.kyori.adventure.text.Component description() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.handle.description());
++ }
++ // Paper end - adventure
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
+index 3bc9e6fae141f7b6f0c8742f9df5b29f64934628..f91577c9239c8d5ed4b72b23dde9c053b4beae0b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
+@@ -60,6 +60,14 @@ public class CraftTrimPattern implements TrimPattern, Handleable<net.minecraft.w
+ @NotNull
+ @Override
+ public String getTranslationKey() {
++ if (!(this.handle.description().getContents() instanceof TranslatableContents)) throw new UnsupportedOperationException("Description isn't translatable!"); // Paper
+ return ((TranslatableContents) this.handle.description().getContents()).getKey();
+ }
++
++ // Paper start - adventure
++ @Override
++ public net.kyori.adventure.text.Component description() {
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.handle.description());
++ }
++ // Paper end - adventure
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java
+index ed4415f6dd588c08c922efd5beebb3b124beb9d6..78a7ac47f20e84ccd67ff44d0bc7a2f2faa0d476 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftCustomInventoryConverter.java
+@@ -12,6 +12,13 @@ public class CraftCustomInventoryConverter implements CraftInventoryCreator.Inve
+ return new CraftInventoryCustom(holder, type);
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ return new CraftInventoryCustom(owner, type, title);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) {
+ return new CraftInventoryCustom(owner, type, title);
+@@ -21,6 +28,12 @@ public class CraftCustomInventoryConverter implements CraftInventoryCreator.Inve
+ return new CraftInventoryCustom(owner, size);
+ }
+
++ // Paper start
++ public Inventory createInventory(InventoryHolder owner, int size, net.kyori.adventure.text.Component title) {
++ return new CraftInventoryCustom(owner, size, title);
++ }
++ // Paper end
++
+ public Inventory createInventory(InventoryHolder owner, int size, String title) {
+ return new CraftInventoryCustom(owner, size, title);
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java
+index ec8ef47ed7cc627fef2c71b2b281119245e88b97..53cbc743b1e722d029021f9d63ffbf7d0fddd04e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftInventoryCreator.java
+@@ -45,6 +45,12 @@ public final class CraftInventoryCreator {
+ return this.converterMap.get(type).createInventory(holder, type);
+ }
+
++ // Paper start
++ public Inventory createInventory(InventoryHolder holder, InventoryType type, net.kyori.adventure.text.Component title) {
++ return converterMap.get(type).createInventory(holder, type, title);
++ }
++ // Paper end
++
+ public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) {
+ return this.converterMap.get(type).createInventory(holder, type, title);
+ }
+@@ -53,6 +59,12 @@ public final class CraftInventoryCreator {
+ return this.DEFAULT_CONVERTER.createInventory(holder, size);
+ }
+
++ // Paper start
++ public Inventory createInventory(InventoryHolder holder, int size, net.kyori.adventure.text.Component title) {
++ return DEFAULT_CONVERTER.createInventory(holder, size, title);
++ }
++ // Paper end
++
+ public Inventory createInventory(InventoryHolder holder, int size, String title) {
+ return this.DEFAULT_CONVERTER.createInventory(holder, size, title);
+ }
+@@ -61,6 +73,10 @@ public final class CraftInventoryCreator {
+
+ Inventory createInventory(InventoryHolder holder, InventoryType type);
+
++ // Paper start
++ Inventory createInventory(InventoryHolder holder, InventoryType type, net.kyori.adventure.text.Component title);
++ // Paper end
++
+ Inventory createInventory(InventoryHolder holder, InventoryType type, String title);
+ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java
+index 11d23b3a9a0c99ede04f2cd64d8022b2b0b737df..7bc082d08a3d577481046818f0d58133413fc723 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftTileInventoryConverter.java
+@@ -31,6 +31,18 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat
+ return this.getInventory(this.getTileEntity());
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ Container te = getTileEntity();
++ if (te instanceof RandomizableContainerBlockEntity) {
++ ((RandomizableContainerBlockEntity) te).name = io.papermc.paper.adventure.PaperAdventure.asVanilla(title);
++ }
++
++ return getInventory(te);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) {
+ Container te = this.getTileEntity();
+@@ -53,6 +65,15 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat
+ return furnace;
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ Container tileEntity = getTileEntity();
++ ((AbstractFurnaceBlockEntity) tileEntity).setCustomName(io.papermc.paper.adventure.PaperAdventure.asVanilla(title));
++ return getInventory(tileEntity);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) {
+ Container tileEntity = this.getTileEntity();
+@@ -73,6 +94,18 @@ public abstract class CraftTileInventoryConverter implements CraftInventoryCreat
+ return new BrewingStandBlockEntity(BlockPos.ZERO, Blocks.BREWING_STAND.defaultBlockState());
+ }
+
++ // Paper start
++ @Override
++ public Inventory createInventory(InventoryHolder owner, InventoryType type, net.kyori.adventure.text.Component title) {
++ // BrewingStand does not extend TileEntityLootable
++ Container tileEntity = getTileEntity();
++ if (tileEntity instanceof BrewingStandBlockEntity) {
++ ((BrewingStandBlockEntity) tileEntity).name = io.papermc.paper.adventure.PaperAdventure.asVanilla(title);
++ }
++ return getInventory(tileEntity);
++ }
++ // Paper end
++
+ @Override
+ public Inventory createInventory(InventoryHolder holder, InventoryType type, String title) {
+ // BrewingStand does not extend TileEntityLootable
+diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java
+index 9efd33cc208af7068be6cf4040dd398f2506c709..0cbbd915631904fe8c6effefb92895422b33eff6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java
++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java
+@@ -43,7 +43,7 @@ public class CraftMapRenderer extends MapRenderer {
+ }
+
+ MapDecoration decoration = this.worldMap.decorations.get(key);
+- cursors.addCursor(new MapCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), CraftMapCursor.CraftType.minecraftHolderToBukkit(decoration.type()), true, CraftChatMessage.fromComponent(decoration.name().orElse(null))));
++ cursors.addCursor(new MapCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), CraftMapCursor.CraftType.minecraftHolderToBukkit(decoration.type()), true, decoration.name().isEmpty() ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(decoration.name().get()))); // Paper
+ }
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java
+index 5c987c7d9e43bb481800935cbc918a43a3656524..da1e4496d78a2c1b258ff8bb316414cb8a662ba2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java
++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java
+@@ -31,6 +31,21 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective
+ return this.objective.getName();
+ }
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component displayName() throws IllegalStateException {
++ CraftScoreboard scoreboard = checkState();
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(objective.getDisplayName());
++ }
++ @Override
++ public void displayName(net.kyori.adventure.text.Component displayName) throws IllegalStateException, IllegalArgumentException {
++ if (displayName == null) {
++ displayName = net.kyori.adventure.text.Component.empty();
++ }
++ CraftScoreboard scoreboard = checkState();
++ objective.setDisplayName(io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName));
++ }
++ // Paper end
+ @Override
+ public String getDisplayName() {
+ this.checkState();
+diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java
+index e8be35f8a7c017164c91a4f794105b3cc5ea2f41..5681630159bb52628e6cc391db324bbafe333414 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java
++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java
+@@ -29,6 +29,33 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard {
+ public CraftObjective registerNewObjective(String name, String criteria) {
+ return this.registerNewObjective(name, criteria, name);
+ }
++ // Paper start - Adventure
++ @Override
++ public CraftObjective registerNewObjective(String name, String criteria, net.kyori.adventure.text.Component displayName) {
++ return this.registerNewObjective(name, CraftCriteria.getFromBukkit(criteria), displayName, RenderType.INTEGER);
++ }
++ @Override
++ public CraftObjective registerNewObjective(String name, String criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) {
++ return this.registerNewObjective(name, CraftCriteria.getFromBukkit(criteria), displayName, renderType);
++ }
++ @Override
++ public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName) throws IllegalArgumentException {
++ return this.registerNewObjective(name, criteria, displayName, RenderType.INTEGER);
++ }
++ @Override
++ public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) throws IllegalArgumentException {
++ if (displayName == null) {
++ displayName = net.kyori.adventure.text.Component.empty();
++ }
++ Preconditions.checkArgument(name != null, "Objective name cannot be null");
++ Preconditions.checkArgument(criteria != null, "Criteria cannot be null");
++ Preconditions.checkArgument(renderType != null, "RenderType cannot be null");
++ Preconditions.checkArgument(name.length() <= Short.MAX_VALUE, "The name '%s' is longer than the limit of 32767 characters (%s)", name, name.length());
++ Preconditions.checkArgument(this.board.getObjective(name) == null, "An objective of name '%s' already exists", name);
++ net.minecraft.world.scores.Objective objective = this.board.addObjective(name, ((CraftCriteria) criteria).criteria, io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName), CraftScoreboardTranslations.fromBukkitRender(renderType), true, null);
++ return new CraftObjective(this, objective);
++ }
++ // Paper end - Adventure
+
+ @Override
+ public CraftObjective registerNewObjective(String name, String criteria, String displayName) {
+@@ -47,15 +74,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard {
+
+ @Override
+ public CraftObjective registerNewObjective(String name, Criteria criteria, String displayName, RenderType renderType) {
+- Preconditions.checkArgument(name != null, "Objective name cannot be null");
+- Preconditions.checkArgument(criteria != null, "Criteria cannot be null");
+- Preconditions.checkArgument(displayName != null, "Display name cannot be null");
+- Preconditions.checkArgument(renderType != null, "RenderType cannot be null");
+- Preconditions.checkArgument(name.length() <= Short.MAX_VALUE, "The name '%s' is longer than the limit of 32767 characters (%s)", name, name.length());
+- Preconditions.checkArgument(this.board.getObjective(name) == null, "An objective of name '%s' already exists", name);
+-
+- net.minecraft.world.scores.Objective objective = this.board.addObjective(name, ((CraftCriteria) criteria).criteria, CraftChatMessage.fromStringOrEmpty(displayName), CraftScoreboardTranslations.fromBukkitRender(renderType), true, null);
+- return new CraftObjective(this, objective);
++ return this.registerNewObjective(name, criteria, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(displayName), renderType); // Paper - Adventure
+ }
+
+ @Override
+diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java
+index 4d586e1375ed8782939c9d480479e0dd981f8cbc..7900adb0b158bc17dd792dd082c338547bc1aa0a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java
++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftTeam.java
+@@ -26,6 +26,63 @@ final class CraftTeam extends CraftScoreboardComponent implements Team {
+
+ return this.team.getName();
+ }
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.Component displayName() throws IllegalStateException {
++ CraftScoreboard scoreboard = checkState();
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getDisplayName());
++ }
++ @Override
++ public void displayName(net.kyori.adventure.text.Component displayName) throws IllegalStateException, IllegalArgumentException {
++ if (displayName == null) displayName = net.kyori.adventure.text.Component.empty();
++ CraftScoreboard scoreboard = checkState();
++ team.setDisplayName(io.papermc.paper.adventure.PaperAdventure.asVanilla(displayName));
++ }
++ @Override
++ public net.kyori.adventure.text.Component prefix() throws IllegalStateException {
++ CraftScoreboard scoreboard = checkState();
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getPlayerPrefix());
++ }
++ @Override
++ public void prefix(net.kyori.adventure.text.Component prefix) throws IllegalStateException, IllegalArgumentException {
++ if (prefix == null) prefix = net.kyori.adventure.text.Component.empty();
++ CraftScoreboard scoreboard = checkState();
++ team.setPlayerPrefix(io.papermc.paper.adventure.PaperAdventure.asVanilla(prefix));
++ }
++ @Override
++ public net.kyori.adventure.text.Component suffix() throws IllegalStateException {
++ CraftScoreboard scoreboard = checkState();
++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(team.getPlayerSuffix());
++ }
++ @Override
++ public void suffix(net.kyori.adventure.text.Component suffix) throws IllegalStateException, IllegalArgumentException {
++ if (suffix == null) suffix = net.kyori.adventure.text.Component.empty();
++ CraftScoreboard scoreboard = checkState();
++ team.setPlayerSuffix(io.papermc.paper.adventure.PaperAdventure.asVanilla(suffix));
++ }
++ @Override
++ public boolean hasColor() {
++ CraftScoreboard scoreboard = checkState();
++ return this.team.getColor().getColor() != null;
++ }
++ @Override
++ public net.kyori.adventure.text.format.TextColor color() throws IllegalStateException {
++ CraftScoreboard scoreboard = checkState();
++ if (team.getColor().getColor() == null) throw new IllegalStateException("Team colors must have hex values");
++ net.kyori.adventure.text.format.TextColor color = net.kyori.adventure.text.format.TextColor.color(team.getColor().getColor());
++ if (!(color instanceof net.kyori.adventure.text.format.NamedTextColor)) throw new IllegalStateException("Team doesn't have a NamedTextColor");
++ return (net.kyori.adventure.text.format.NamedTextColor) color;
++ }
++ @Override
++ public void color(net.kyori.adventure.text.format.NamedTextColor color) {
++ CraftScoreboard scoreboard = checkState();
++ if (color == null) {
++ this.team.setColor(net.minecraft.ChatFormatting.RESET);
++ } else {
++ this.team.setColor(io.papermc.paper.adventure.PaperAdventure.asVanilla(color));
++ }
++ }
++ // Paper end
+
+ @Override
+ public String getDisplayName() {
+@@ -303,4 +360,20 @@ final class CraftTeam extends CraftScoreboardComponent implements Team {
+ return !(this.team != other.team && (this.team == null || !this.team.equals(other.team)));
+ }
+
++ // Paper start - make Team extend ForwardingAudience
++ @Override
++ public @org.jetbrains.annotations.NotNull Iterable<? extends net.kyori.adventure.audience.Audience> audiences() {
++ this.checkState();
++ java.util.List<net.kyori.adventure.audience.Audience> audiences = new java.util.ArrayList<>();
++ for (String playerName : this.team.getPlayers()) {
++ org.bukkit.entity.Player player = Bukkit.getPlayerExact(playerName);
++ if (player != null) {
++ audiences.add(player);
++ }
++ }
++
++ return audiences;
++ }
++ // Paper end - make Team extend ForwardingAudience
++
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java
+index ff040613083c015d9c52c0995591b64305fd5018..95444fd9fecc5bda5462ca8dfeca82c5318f0895 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java
+@@ -90,7 +90,7 @@ public final class CraftChatMessage {
+ this.hex.append(c);
+
+ if (this.hex.length() == 7) {
+- this.modifier = StringMessage.RESET.withColor(TextColor.parseColor(this.hex.toString()).result().get());
++ this.modifier = StringMessage.RESET.withColor(TextColor.parseColor(this.hex.toString()).result().orElse(null)); // Paper
+ this.hex = null;
+ }
+ } else if (format.isFormat() && format != ChatFormatting.RESET) {
+@@ -264,6 +264,7 @@ public final class CraftChatMessage {
+
+ public static String fromComponent(Component component) {
+ if (component == null) return "";
++ if (component instanceof io.papermc.paper.adventure.AdventureComponent) component = ((io.papermc.paper.adventure.AdventureComponent) component).deepConverted();
+ StringBuilder out = new StringBuilder();
+
+ boolean hadFormat = false;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index de7f9d5b3860e7d187d73a1bd0d28c70293ef66c..bcc9c0295495301d3b62ceb9d4ea93e365caee87 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -80,6 +80,43 @@ public final class CraftMagicNumbers implements UnsafeValues {
+
+ private CraftMagicNumbers() {}
+
++ // Paper start
++ @Override
++ public net.kyori.adventure.text.flattener.ComponentFlattener componentFlattener() {
++ return io.papermc.paper.adventure.PaperAdventure.FLATTENER;
++ }
++
++ @Override
++ public net.kyori.adventure.text.serializer.gson.GsonComponentSerializer colorDownsamplingGsonComponentSerializer() {
++ return net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.colorDownsamplingGson();
++ }
++
++ @Override
++ public net.kyori.adventure.text.serializer.gson.GsonComponentSerializer gsonComponentSerializer() {
++ return net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson();
++ }
++
++ @Override
++ public net.kyori.adventure.text.serializer.plain.PlainComponentSerializer plainComponentSerializer() {
++ return io.papermc.paper.adventure.PaperAdventure.PLAIN;
++ }
++
++ @Override
++ public net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer plainTextSerializer() {
++ return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText();
++ }
++
++ @Override
++ public net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer legacyComponentSerializer() {
++ return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection();
++ }
++
++ @Override
++ public net.kyori.adventure.text.Component resolveWithContext(final net.kyori.adventure.text.Component component, final org.bukkit.command.CommandSender context, final org.bukkit.entity.Entity scoreboardSubject, final boolean bypassPermissions) throws IOException {
++ return io.papermc.paper.adventure.PaperAdventure.resolveWithContext(component, context, scoreboardSubject, bypassPermissions);
++ }
++ // Paper end
++
+ public static BlockState getBlock(MaterialData material) {
+ return CraftMagicNumbers.getBlock(material.getItemType(), material.getData());
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
+index 62c66e3179b9557cdba46242df0fb15bce7e7710..73a37638abacdffbff8274291a64ea6cd0be7a5e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
+@@ -80,7 +80,7 @@ public abstract class LazyHashSet<E> implements Set<E> {
+ return this.reference = this.makeReference();
+ }
+
+- abstract Set<E> makeReference();
++ protected abstract Set<E> makeReference(); // Paper - protected
+
+ public boolean isLazy() {
+ return this.reference == null;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+index 0ab4c7eaffe69b314423732dd529aaeafc476e08..8a44e7260518bda87c6d0eeade98d5b81a04c3b6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+@@ -16,9 +16,14 @@ public class LazyPlayerSet extends LazyHashSet<Player> {
+ }
+
+ @Override
+- HashSet<Player> makeReference() {
++ protected HashSet<Player> makeReference() { // Paper - protected
+ Preconditions.checkState(this.reference == null, "Reference already created!");
+- List<ServerPlayer> players = this.server.getPlayerList().players;
++ // Paper start
++ return makePlayerSet(this.server);
++ }
++ public static HashSet<Player> makePlayerSet(final MinecraftServer server) {
++ List<ServerPlayer> players = server.getPlayerList().players;
++ // Paper end
+ HashSet<Player> reference = new HashSet<Player>(players.size());
+ for (ServerPlayer player : players) {
+ reference.add(player.getBukkitEntity());
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..9b7119d0b88bf7f9d25fab37a15340cabc0c9b7b
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.bossbar.BossBarImplementation$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.BossBarImplementationProvider
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..845711e03c41c6b6a03d541f1c43d37b24c11733
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.ClickCallback$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.ClickCallbackProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..714cb03596627badb6ad7f23b17f2e686761a9b5
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.event.DataComponentValueConverterRegistry$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.DataComponentValueConverterProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider
+new file mode 100644
+index 0000000000000000000000000000000000000000..399bde6e57cd82b50d3ebe0f51a3958fa2d52d43
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.ComponentLoggerProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..6ce632b6c9dc5e4b3b978331df51c0ffd1526471
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.minimessage.MiniMessage$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.MiniMessageProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..bc9f7398a0fe158af05b562a8ded9e74a22eae9b
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.gson.GsonComponentSerializer$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.GsonComponentSerializerProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..820f381981a91754b7f0c5106f93b773d885e321
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.LegacyComponentSerializerProviderImpl
+diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider
+new file mode 100644
+index 0000000000000000000000000000000000000000..28d777610b52ba74f808bf3245d73b8333d01fa7
+--- /dev/null
++++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer$Provider
+@@ -0,0 +1 @@
++io.papermc.paper.adventure.providers.PlainTextComponentSerializerProviderImpl
+diff --git a/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json b/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json
+new file mode 100644
+index 0000000000000000000000000000000000000000..3aedd0bbc97edacc1ebf71264b310e55aaaa5cb3
+--- /dev/null
++++ b/src/main/resources/data/minecraft/datapacks/paper/data/paper/chat_type/raw.json
+@@ -0,0 +1,14 @@
++{
++ "chat": {
++ "parameters": [
++ "content"
++ ],
++ "translation_key": "%s"
++ },
++ "narration": {
++ "parameters": [
++ "content"
++ ],
++ "translation_key": "%s"
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..074e46aa4aca1a5154a0198279f60b38e05c1344
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/adventure/AdventureCodecsTest.java
+@@ -0,0 +1,405 @@
++package io.papermc.paper.adventure;
++
++import com.mojang.datafixers.util.Pair;
++import com.mojang.serialization.Codec;
++import com.mojang.serialization.DataResult;
++import com.mojang.serialization.DynamicOps;
++import com.mojang.serialization.JavaOps;
++import com.mojang.serialization.JsonOps;
++import io.papermc.paper.util.MethodParameterSource;
++import java.io.IOException;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.util.List;
++import java.util.UUID;
++import java.util.function.Function;
++import java.util.stream.Stream;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.text.BlockNBTComponent;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.event.HoverEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.Style;
++import net.kyori.adventure.text.format.TextColor;
++import net.kyori.adventure.text.format.TextDecoration;
++import net.minecraft.core.component.DataComponents;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.nbt.ByteTag;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.IntTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.network.chat.ComponentSerialization;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.world.item.ItemStack;
++import net.minecraft.world.item.Items;
++import org.apache.commons.lang3.RandomStringUtils;
++import org.bukkit.support.RegistryHelper;
++import org.bukkit.support.environment.VanillaFeature;
++import org.junit.jupiter.api.Test;
++import org.junit.jupiter.params.ParameterizedTest;
++import org.junit.jupiter.params.provider.EnumSource;
++import org.junit.jupiter.params.provider.MethodSource;
++import org.junitpioneer.jupiter.cartesian.CartesianTest;
++
++import static io.papermc.paper.adventure.AdventureCodecs.CLICK_EVENT_CODEC;
++import static io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC;
++import static io.papermc.paper.adventure.AdventureCodecs.HOVER_EVENT_CODEC;
++import static io.papermc.paper.adventure.AdventureCodecs.KEY_CODEC;
++import static io.papermc.paper.adventure.AdventureCodecs.STYLE_MAP_CODEC;
++import static io.papermc.paper.adventure.AdventureCodecs.TEXT_COLOR_CODEC;
++import static java.util.Objects.requireNonNull;
++import static net.kyori.adventure.key.Key.key;
++import static net.kyori.adventure.text.Component.blockNBT;
++import static net.kyori.adventure.text.Component.entityNBT;
++import static net.kyori.adventure.text.Component.keybind;
++import static net.kyori.adventure.text.Component.score;
++import static net.kyori.adventure.text.Component.selector;
++import static net.kyori.adventure.text.Component.storageNBT;
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.Component.translatable;
++import static net.kyori.adventure.text.TranslationArgument.numeric;
++import static net.kyori.adventure.text.event.ClickEvent.openUrl;
++import static net.kyori.adventure.text.event.ClickEvent.suggestCommand;
++import static net.kyori.adventure.text.event.HoverEvent.showEntity;
++import static net.kyori.adventure.text.format.Style.style;
++import static net.kyori.adventure.text.format.TextColor.color;
++import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
++import static org.junit.jupiter.api.Assertions.assertEquals;
++import static org.junit.jupiter.api.Assertions.assertNotNull;
++import static org.junit.jupiter.api.Assertions.assertThrows;
++import static org.junit.jupiter.api.Assertions.assertTrue;
++
++@VanillaFeature
++class AdventureCodecsTest {
++
++ static final String PARAMETERIZED_NAME = "[{index}] {displayName}: {arguments}";
++
++ @Test
++ void testTextColor() {
++ final TextColor color = color(0x1d38df);
++ final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow();
++ assertEquals(color.asHexString(), result.getAsString());
++ final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
++ assertEquals(color.value(), nms.getValue());
++ }
++
++ @Test
++ void testNamedTextColor() {
++ final NamedTextColor color = NamedTextColor.BLUE;
++ final Tag result = TEXT_COLOR_CODEC.encodeStart(NbtOps.INSTANCE, color).result().orElseThrow();
++ assertEquals(NamedTextColor.NAMES.keyOrThrow(color), result.getAsString());
++ final net.minecraft.network.chat.TextColor nms = net.minecraft.network.chat.TextColor.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
++ assertEquals(color.value(), nms.getValue());
++ }
++
++ @Test
++ void testKey() {
++ final Key key = key("hello", "there");
++ final Tag result = KEY_CODEC.encodeStart(NbtOps.INSTANCE, key).result().orElseThrow();
++ assertEquals(key.asString(), result.getAsString());
++ final ResourceLocation location = ResourceLocation.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
++ assertEquals(key.asString(), location.toString());
++ }
++
++ @ParameterizedTest(name = PARAMETERIZED_NAME)
++ @EnumSource(value = ClickEvent.Action.class, mode = EnumSource.Mode.EXCLUDE, names = {"OPEN_FILE"})
++ void testClickEvent(final ClickEvent.Action action) {
++ final ClickEvent event = ClickEvent.clickEvent(action, RandomStringUtils.randomAlphanumeric(20));
++ final Tag result = CLICK_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, event).result().orElseThrow();
++ final net.minecraft.network.chat.ClickEvent nms = net.minecraft.network.chat.ClickEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
++ assertEquals(event.action().toString(), nms.getAction().getSerializedName());
++ assertEquals(event.value(), nms.getValue());
++ }
++
++ @Test
++ void testShowTextHoverEvent() {
++ final HoverEvent<Component> hoverEvent = HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, text("hello"));
++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
++ final net.minecraft.network.chat.HoverEvent nms = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result).result().orElseThrow().getFirst();
++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
++ assertNotNull(nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_TEXT));
++ }
++
++ @Test
++ void testShowItemHoverEvent() throws IOException {
++ final ItemStack stack = new ItemStack(Items.PUMPKIN, 3);
++ stack.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("NAME"));
++ final HoverEvent<HoverEvent.ShowItem> hoverEvent = HoverEvent.showItem(key("minecraft:pumpkin"), 3, PaperAdventure.asAdventure(stack.getComponentsPatch()));
++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result);
++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
++ final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst();
++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
++ final net.minecraft.network.chat.HoverEvent.ItemStackInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ITEM);
++ assertNotNull(value);
++ assertEquals(hoverEvent.value().count(), value.count);
++ assertEquals(hoverEvent.value().item().asString(), value.item.unwrapKey().orElseThrow().location().toString());
++ assertEquals(stack.getComponentsPatch(), value.components);
++ }
++
++ @Test
++ void testShowEntityHoverEvent() {
++ UUID uuid = UUID.randomUUID();
++ final HoverEvent<HoverEvent.ShowEntity> hoverEvent = showEntity(key("minecraft:wolf"), uuid, text("NAME"));
++ final Tag result = HOVER_EVENT_CODEC.encodeStart(NbtOps.INSTANCE, hoverEvent).result().orElseThrow();
++ final DataResult<Pair<net.minecraft.network.chat.HoverEvent, Tag>> dataResult = net.minecraft.network.chat.HoverEvent.CODEC.decode(NbtOps.INSTANCE, result);
++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
++ final net.minecraft.network.chat.HoverEvent nms = dataResult.result().orElseThrow().getFirst();
++ assertEquals(hoverEvent.action().toString(), nms.getAction().getSerializedName());
++ final net.minecraft.network.chat.HoverEvent.EntityTooltipInfo value = nms.getValue(net.minecraft.network.chat.HoverEvent.Action.SHOW_ENTITY);
++ assertNotNull(value);
++ assertEquals(hoverEvent.value().type().asString(), BuiltInRegistries.ENTITY_TYPE.getKey(value.type).toString());
++ assertEquals(hoverEvent.value().id(), value.id);
++ assertEquals("NAME", value.name.orElseThrow().getString());
++ }
++
++ @Test
++ void testSimpleStyle() {
++ final Style style = style().decorate(TextDecoration.BOLD).color(NamedTextColor.RED).build();
++ final Tag result = STYLE_MAP_CODEC.codec().encodeStart(NbtOps.INSTANCE, style).result().orElseThrow();
++ final DataResult<Pair<net.minecraft.network.chat.Style, Tag>> dataResult = net.minecraft.network.chat.Style.Serializer.CODEC.decode(NbtOps.INSTANCE, result);
++ assertTrue(dataResult.result().isPresent(), () -> dataResult + " result is not present");
++ final net.minecraft.network.chat.Style nms = dataResult.result().get().getFirst();
++ assertTrue(nms.isBold());
++ assertEquals(requireNonNull(style.color()).value(), requireNonNull(nms.getColor()).getValue());
++ }
++
++ @CartesianTest(name = PARAMETERIZED_NAME)
++ void testDirectRoundTripStyle(
++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++ @MethodParameterSource("testStyles") final Style style
++ ) {
++ testDirectRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), style);
++ }
++
++ @CartesianTest(name = PARAMETERIZED_NAME)
++ void testMinecraftRoundTripStyle(
++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++ @MethodParameterSource("testStyles") final Style style
++ ) {
++ testMinecraftRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style);
++ }
++
++ @CartesianTest(name = PARAMETERIZED_NAME)
++ void testDirectRoundTripComponent(
++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++ @TestComponents final Component component
++ ) {
++ testDirectRoundTrip(dynamicOps, COMPONENT_CODEC, component);
++ }
++
++ @CartesianTest(name = PARAMETERIZED_NAME)
++ void testMinecraftRoundTripComponent(
++ @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++ @TestComponents final Component component
++ ) {
++ testMinecraftRoundTrip(dynamicOps, COMPONENT_CODEC, ComponentSerialization.CODEC, component);
++ }
++
++ static List<? extends DynamicOps<?>> dynamicOps() {
++ return Stream.of(
++ NbtOps.INSTANCE,
++ JavaOps.INSTANCE,
++ JsonOps.INSTANCE
++ )
++ .map(ops -> RegistryHelper.getRegistry().createSerializationContext(ops))
++ .toList();
++ }
++
++ @ParameterizedTest(name = PARAMETERIZED_NAME)
++ @MethodSource({"invalidData"})
++ void invalidThrows(final Tag input) {
++ assertThrows(RuntimeException.class, () -> {
++ require(
++ COMPONENT_CODEC.decode(NbtOps.INSTANCE, input),
++ msg -> "Failed to decode " + input + ": " + msg
++ );
++ });
++ }
++
++ static <A, O> void testDirectRoundTrip(final DynamicOps<O> ops, final Codec<A> codec, final A adventure) {
++ final O encoded = require(
++ codec.encodeStart(ops, adventure),
++ msg -> "Failed to encode " + adventure + ": " + msg
++ );
++ final Pair<A, O> roundTripResult = require(
++ codec.decode(ops, encoded),
++ msg -> "Failed to decode " + encoded + ": " + msg
++ );
++ assertEquals(adventure, roundTripResult.getFirst());
++ }
++
++ static <A, M, O> void testMinecraftRoundTrip(final DynamicOps<O> ops, final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) {
++ final O encoded = require(
++ adventureCodec.encodeStart(ops, adventure),
++ msg -> "Failed to encode " + adventure + ": " + msg
++ );
++ final M minecraftResult = require(
++ minecraftCodec.decode(ops, encoded),
++ msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg
++ ).getFirst();
++ final O minecraftReEncoded = require(
++ minecraftCodec.encodeStart(ops, minecraftResult),
++ msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg
++ );
++ final Pair<A, O> roundTripResult = require(
++ adventureCodec.decode(ops, minecraftReEncoded),
++ msg -> "Failed to decode " + minecraftReEncoded + ": " + msg
++ );
++ assertEquals(adventure, roundTripResult.getFirst());
++ }
++
++ static <R> R require(final DataResult<R> result, final Function<String, String> errorMessage) {
++ return result.getOrThrow(s -> new RuntimeException(errorMessage.apply(s)));
++ }
++
++ static List<Tag> invalidData() {
++ return List.of(
++ IntTag.valueOf(-1),
++ ByteTag.ZERO,
++ new CompoundTag(),
++ new ListTag()
++ );
++ }
++
++ static List<Style> testStyles() {
++ return List.of(
++ Style.empty(),
++ style(color(0x0a1ab9)),
++ style(NamedTextColor.LIGHT_PURPLE),
++ style(TextDecoration.BOLD),
++ style(TextDecoration.BOLD.withState(false)),
++ style(TextDecoration.BOLD.withState(TextDecoration.State.NOT_SET)),
++ style()
++ .font(key("kyori", "kittens"))
++ .color(NamedTextColor.RED)
++ .decoration(TextDecoration.BOLD, true)
++ .clickEvent(openUrl("https://github.com"))
++ .build(),
++ style()
++ .hoverEvent(HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity(
++ Key.key(Key.MINECRAFT_NAMESPACE, "pig"),
++ UUID.randomUUID(),
++ Component.text("Dolores", TextColor.color(0x0a1ab9))
++ )))
++ .build()
++ );
++ }
++
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
++ @MethodParameterSource({
++ "testTexts", "testTranslatables", "testKeybinds", "testScores",
++ "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"
++ })
++ @interface TestComponents {
++ }
++
++ static List<Component> testTexts() {
++ return List.of(
++ Component.empty(),
++ text("Hello, world."),
++ text().content("c")
++ .color(NamedTextColor.GOLD)
++ .append(text("o", NamedTextColor.DARK_AQUA))
++ .append(text("l", NamedTextColor.LIGHT_PURPLE))
++ .append(text("o", NamedTextColor.DARK_PURPLE))
++ .append(text("u", NamedTextColor.BLUE))
++ .append(text("r", NamedTextColor.DARK_GREEN))
++ .append(text("s", NamedTextColor.RED))
++ .build(),
++ text().content("This is a test.")
++ .color(NamedTextColor.DARK_PURPLE)
++ .hoverEvent(HoverEvent.showText(text("A test.")))
++ .append(text(" "))
++ .append(text("A what?", NamedTextColor.DARK_AQUA))
++ .build(),
++ text().append(text("Hello")).build(),
++ miniMessage().deserialize("<rainbow>|||||||||||||||||||||||<bold>|||||||||||||</bold>|||||||||")
++ );
++ }
++
++ static List<Component> testTranslatables() {
++ final String key = "multiplayer.player.left";
++ final UUID id = UUID.fromString("eb121687-8b1a-4944-bd4d-e0a818d9dfe2");
++ final String name = "kashike";
++ final String command = String.format("/msg %s ", name);
++
++ return List.of(
++ translatable(key),
++ translatable()
++ .key("thisIsA")
++ .fallback("This is a test.")
++ .build(),
++ translatable(key, numeric(Integer.MAX_VALUE), text("HEY")), // boolean doesn't work in vanilla, can't test here
++ translatable(
++ key,
++ text().content(name)
++ .clickEvent(suggestCommand(command))
++ .hoverEvent(showEntity(HoverEvent.ShowEntity.showEntity(
++ key("minecraft", "player"),
++ id,
++ text(name)
++ )))
++ .build()
++ ).color(NamedTextColor.YELLOW)
++ );
++ }
++
++ static List<Component> testKeybinds() {
++ return List.of(keybind("key.jump"));
++ }
++
++ static List<Component> testScores() {
++ final String name = "abc";
++ final String objective = "def";
++
++ return List.of(score(name, objective));
++ }
++
++ static List<Component> testSelectors() {
++ final String selector = "@p";
++
++ return List.of(
++ selector(selector),
++ selector(selector, text(','))
++ );
++ }
++
++ static List<Component> testBlockNbts() {
++ return List.of(
++ blockNBT().nbtPath("abc").localPos(1.23d, 2.0d, 3.89d).build(),
++ blockNBT().nbtPath("xyz").absoluteWorldPos(4, 5, 6).interpret(true).build(),
++ blockNBT().nbtPath("eeee").relativeWorldPos(7, 83, 900)
++ .separator(text(';'))
++ .build(),
++ blockNBT().nbtPath("qwert").worldPos(
++ BlockNBTComponent.WorldPos.Coordinate.absolute(12),
++ BlockNBTComponent.WorldPos.Coordinate.relative(3),
++ BlockNBTComponent.WorldPos.Coordinate.absolute(1200)
++ ).build()
++ );
++ }
++
++ static List<Component> testEntityNbts() {
++ return List.of(
++ entityNBT().nbtPath("abc").selector("test").build(),
++ entityNBT().nbtPath("abc").selector("test").separator(text(',')).build(),
++ entityNBT().nbtPath("abc").selector("test").interpret(true).build()
++ );
++ }
++
++ static List<Component> testStorageNbts() {
++ return List.of(
++ storageNBT().nbtPath("abc").storage(key("doom:apple")).build(),
++ storageNBT().nbtPath("abc").storage(key("doom:apple")).separator(text(", ")).build(),
++ storageNBT().nbtPath("abc").storage(key("doom:apple")).interpret(true).build()
++ );
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java b/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6ec088c79e3d727bc7e89d1c14b001345feb1f39
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/adventure/ComponentServicesTest.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.adventure;
++
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
++import org.bukkit.support.environment.AllFeatures;
++import org.junit.jupiter.api.Test;
++
++import static org.junit.jupiter.api.Assertions.assertEquals;
++
++@AllFeatures
++public class ComponentServicesTest {
++
++ @Test
++ public void testPlainTextComponentSerializerProvider() {
++ assertEquals("Done", PlainTextComponentSerializer.plainText().serialize(Component.translatable("narrator.loading.done")));
++ }
++
++ @Test
++ public void testLegacyComponentSerializerProvider() {
++ assertEquals("§cDone", LegacyComponentSerializer.legacySection().serialize(Component.translatable("narrator.loading.done", NamedTextColor.RED)));
++ assertEquals("&cDone", LegacyComponentSerializer.legacyAmpersand().serialize(Component.translatable("narrator.loading.done", NamedTextColor.RED)));
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/util/MethodParameterSource.java b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
+index 6cbf11c898439834cffb99ef84e5df1494356809..381668df2f58df82ac6da85796752934e92d0796 100644
+--- a/src/test/java/io/papermc/paper/util/MethodParameterSource.java
++++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
+@@ -4,11 +4,13 @@ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
++import org.intellij.lang.annotations.Language;
+ import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
+ @CartesianArgumentsSource(MethodParameterProvider.class)
+ public @interface MethodParameterSource {
++ @Language("jvm-method-name")
+ String[] value() default {};
+ }
diff --git a/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch
new file mode 100644
index 0000000000..2e317fd247
--- /dev/null
+++ b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch
@@ -0,0 +1,707 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Minecrell <[email protected]>
+Date: Fri, 9 Jun 2017 19:03:43 +0200
+Subject: [PATCH] Use TerminalConsoleAppender for console improvements
+
+Rewrite console improvements (console colors, tab completion,
+persistent input line, ...) using JLine 3.x and TerminalConsoleAppender.
+
+Also uses the new ANSIComponentSerializer to serialize components when
+logging them via the ComponentLogger, or when sending messages to the
+console, for hex color support.
+
+New features:
+ - Support console colors for Vanilla commands
+ - Add console colors for warnings and errors
+ - Server can now be turned off safely using CTRL + C. JLine catches
+ the signal and the implementation shuts down the server cleanly.
+ - Support console colors and persistent input line when running in
+ IntelliJ IDEA
+
+Other changes:
+ - Server starts 1-2 seconds faster thanks to optimizations in Log4j
+ configuration
+
+Co-Authored-By: Emilia Kond <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index b4a389d0ef9df8ef49abb7049037e391d491d0c9..611960836f7f66af17e9a231d85e7181dcc068c8 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -5,6 +5,12 @@ plugins {
+ `maven-publish`
+ }
+
++val log4jPlugins = sourceSets.create("log4jPlugins")
++configurations.named(log4jPlugins.compileClasspathConfigurationName) {
++ extendsFrom(configurations.compileClasspath.get())
++}
++val alsoShade: Configuration by configurations.creating
++
+ // Paper start - configure mockito agent that is needed in newer java versions
+ val mockitoAgent = configurations.register("mockitoAgent")
+ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
+@@ -19,7 +25,22 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
+
+ dependencies {
+ implementation(project(":paper-api"))
+- implementation("jline:jline:2.12.1")
++ // Paper start
++ implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
++ implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21
++ implementation("net.minecrell:terminalconsoleappender:1.3.0")
++ implementation("net.kyori:adventure-text-serializer-ansi:4.17.0") // Keep in sync with adventureVersion from Paper-API build file
++ /*
++ Required to add the missing Log4j2Plugins.dat file from log4j-core
++ which has been removed by Mojang. Without it, log4j has to classload
++ all its classes to check if they are plugins.
++ Scanning takes about 1-2 seconds so adding this speeds up the server start.
++ */
++ runtimeOnly("org.apache.logging.log4j:log4j-core:2.19.0")
++ log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins
++ runtimeOnly(log4jPlugins.output)
++ alsoShade(log4jPlugins.output)
++ // Paper end
+ implementation("org.apache.logging.log4j:log4j-iostreams:2.24.1") // Paper - remove exclusion
+ implementation("org.ow2.asm:asm-commons:9.7.1")
+ implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files
+@@ -92,6 +113,19 @@ tasks.check {
+ dependsOn(scanJar)
+ }
+ // Paper end
++// Paper start - use TCA for console improvements
++tasks.serverJar {
++ from(alsoShade.elements.map {
++ it.map { f ->
++ if (f.asFile.isFile) {
++ zipTree(f.asFile)
++ } else {
++ f.asFile
++ }
++ }
++ })
++}
++// Paper end - use TCA for console improvements
+
+ tasks.test {
+ include("**/**TestSuite.class")
+diff --git a/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java b/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..91547f6e6fe90006713beb2818da634304bdd236
+--- /dev/null
++++ b/src/log4jPlugins/java/io/papermc/paper/console/StripANSIConverter.java
+@@ -0,0 +1,51 @@
++package io.papermc.paper.console;
++
++import org.apache.logging.log4j.core.LogEvent;
++import org.apache.logging.log4j.core.config.Configuration;
++import org.apache.logging.log4j.core.config.plugins.Plugin;
++import org.apache.logging.log4j.core.layout.PatternLayout;
++import org.apache.logging.log4j.core.pattern.ConverterKeys;
++import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
++import org.apache.logging.log4j.core.pattern.PatternConverter;
++import org.apache.logging.log4j.core.pattern.PatternFormatter;
++import org.apache.logging.log4j.core.pattern.PatternParser;
++
++import java.util.List;
++import java.util.regex.Pattern;
++
++@Plugin(name = "stripAnsi", category = PatternConverter.CATEGORY)
++@ConverterKeys({"stripAnsi"})
++public final class StripANSIConverter extends LogEventPatternConverter {
++ final private Pattern ANSI_PATTERN = Pattern.compile("\\e\\[[\\d;]*[^\\d;]");
++
++ private final List<PatternFormatter> formatters;
++
++ private StripANSIConverter(List<PatternFormatter> formatters) {
++ super("stripAnsi", null);
++ this.formatters = formatters;
++ }
++
++ @Override
++ public void format(LogEvent event, StringBuilder toAppendTo) {
++ int start = toAppendTo.length();
++ for (PatternFormatter formatter : formatters) {
++ formatter.format(event, toAppendTo);
++ }
++ String content = toAppendTo.substring(start);
++ content = ANSI_PATTERN.matcher(content).replaceAll("");
++
++ toAppendTo.setLength(start);
++ toAppendTo.append(content);
++ }
++
++ public static StripANSIConverter newInstance(Configuration config, String[] options) {
++ if (options.length != 1) {
++ LOGGER.error("Incorrect number of options on stripAnsi. Expected exactly 1, received " + options.length);
++ return null;
++ }
++
++ PatternParser parser = PatternLayout.createPatternParser(config);
++ List<PatternFormatter> formatters = parser.parse(options[0]);
++ return new StripANSIConverter(formatters);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a4070b59e261f0f1ac4beec47b11492f4724bf27
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+@@ -0,0 +1,41 @@
++package com.destroystokyo.paper.console;
++
++import net.minecraft.server.dedicated.DedicatedServer;
++import net.minecrell.terminalconsole.SimpleTerminalConsole;
++import org.bukkit.craftbukkit.command.ConsoleCommandCompleter;
++import org.jline.reader.LineReader;
++import org.jline.reader.LineReaderBuilder;
++
++public final class PaperConsole extends SimpleTerminalConsole {
++
++ private final DedicatedServer server;
++
++ public PaperConsole(DedicatedServer server) {
++ this.server = server;
++ }
++
++ @Override
++ protected LineReader buildReader(LineReaderBuilder builder) {
++ return super.buildReader(builder
++ .appName("Paper")
++ .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history"))
++ .completer(new ConsoleCommandCompleter(this.server))
++ );
++ }
++
++ @Override
++ protected boolean isRunning() {
++ return !this.server.isStopped() && this.server.isRunning();
++ }
++
++ @Override
++ protected void runCommand(String command) {
++ this.server.handleConsoleInput(command, this.server.createCommandSourceStack());
++ }
++
++ @Override
++ protected void shutdown() {
++ this.server.halt(false);
++ }
++
++}
+diff --git a/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8f07539a82f449ad217e316a7513a1708781fb63
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java
+@@ -0,0 +1,26 @@
++package com.destroystokyo.paper.console;
++
++import net.kyori.adventure.audience.MessageType;
++import net.kyori.adventure.identity.Identity;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import org.apache.logging.log4j.LogManager;
++import org.bukkit.craftbukkit.command.CraftConsoleCommandSender;
++
++public class TerminalConsoleCommandSender extends CraftConsoleCommandSender {
++
++ private static final ComponentLogger LOGGER = ComponentLogger.logger(LogManager.getRootLogger().getName());
++
++ @Override
++ public void sendRawMessage(String message) {
++ final Component msg = LegacyComponentSerializer.legacySection().deserialize(message);
++ this.sendMessage(Identity.nil(), msg, MessageType.SYSTEM);
++ }
++
++ @Override
++ public void sendMessage(Identity identity, Component message, MessageType type) {
++ LOGGER.info(message);
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
+index 8ec506a1ae40f2e4b01af9b34a0b98be8653b460..f466bfdf5557c94ebee3ad609d9b6f18f86aefef 100644
+--- a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
++++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
+@@ -31,6 +31,7 @@ import net.kyori.adventure.text.flattener.ComponentFlattener;
+ import net.kyori.adventure.text.format.Style;
+ import net.kyori.adventure.text.format.TextColor;
+ import net.kyori.adventure.text.serializer.ComponentSerializer;
++import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
+ import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+ import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
+ import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+@@ -129,6 +130,7 @@ public final class PaperAdventure {
+ public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger
+ @Deprecated
+ public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build();
++ public static final ANSIComponentSerializer ANSI_SERIALIZER = ANSIComponentSerializer.builder().flattener(FLATTENER).build();
+ public static final Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() {
+ @Override
+ public @NotNull Tag decode(final @NotNull String encoded) throws CommandSyntaxException {
+diff --git a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java
+index 8323f135d6bf2e1f12525e05094ffa3f2420e7e1..a143ea1e58464a3122fbd8ccafe417bdb3c31c78 100644
+--- a/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java
++++ b/src/main/java/io/papermc/paper/adventure/providers/ComponentLoggerProviderImpl.java
+@@ -1,9 +1,11 @@
+ package io.papermc.paper.adventure.providers;
+
+ import io.papermc.paper.adventure.PaperAdventure;
++import java.util.Locale;
+ import net.kyori.adventure.text.Component;
+ import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
+ import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
++import net.kyori.adventure.translation.GlobalTranslator;
+ import org.jetbrains.annotations.NotNull;
+ import org.slf4j.LoggerFactory;
+
+@@ -15,6 +17,6 @@ public class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
+ }
+
+ private String serialize(final Component message) {
+- return PaperAdventure.asPlain(message, null);
++ return PaperAdventure.ANSI_SERIALIZER.serialize(GlobalTranslator.render(message, Locale.getDefault()));
+ }
+ }
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 8c68969b7d22376cfe5aadf81a16d9ba45e7c131..f43958a5253af0e753ba2b7d5ee9e715a3eaa424 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -161,7 +161,7 @@ import com.mojang.serialization.Dynamic;
+ import com.mojang.serialization.Lifecycle;
+ import java.io.File;
+ import java.util.Random;
+-import jline.console.ConsoleReader;
++// import jline.console.ConsoleReader; // Paper
+ import joptsimple.OptionSet;
+ import net.minecraft.nbt.NbtException;
+ import net.minecraft.nbt.ReportedNbtException;
+@@ -307,7 +307,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ public org.bukkit.craftbukkit.CraftServer server;
+ public OptionSet options;
+ public org.bukkit.command.ConsoleCommandSender console;
+- public ConsoleReader reader;
+ public static int currentTick = (int) (System.currentTimeMillis() / 50);
+ public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
+ public int autosavePeriod;
+@@ -398,7 +397,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.options = options;
+ this.worldLoader = worldLoader;
+ this.vanillaCommandDispatcher = worldstem.dataPackResources().commands; // CraftBukkit
++ // Paper start - Handled by TerminalConsoleAppender
+ // Try to see if we're actually running in a terminal, disable jline if not
++ /*
+ if (System.console() == null && System.getProperty("jline.terminal") == null) {
+ System.setProperty("jline.terminal", "jline.UnsupportedTerminal");
+ Main.useJline = false;
+@@ -419,6 +420,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ MinecraftServer.LOGGER.warn((String) null, ex);
+ }
+ }
++ */
++ // Paper end
+ Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
+ // CraftBukkit end
+ this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
+@@ -1159,7 +1162,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ org.spigotmc.WatchdogThread.doStop(); // Spigot
+ // CraftBukkit start - Restore terminal to original settings
+ try {
+- this.reader.getTerminal().restore();
++ net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
+ } catch (Exception ignored) {
+ }
+ // CraftBukkit end
+@@ -1744,7 +1747,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ @Override
+ public void sendSystemMessage(Component message) {
+- MinecraftServer.LOGGER.info(message.getString());
++ MinecraftServer.LOGGER.info(io.papermc.paper.adventure.PaperAdventure.ANSI_SERIALIZER.serialize(io.papermc.paper.adventure.PaperAdventure.asAdventure(message))); // Paper - Log message with colors
+ }
+
+ public KeyPair getKeyPair() {
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index ca095f9d6c985b066a393debc6529973a3616397..dcb9258d3fbdfdfd41065d4c0919ed4300eac3ae 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -111,6 +111,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ if (!org.bukkit.craftbukkit.Main.useConsole) {
+ return;
+ }
++ // Paper start - Use TerminalConsoleAppender
++ new com.destroystokyo.paper.console.PaperConsole(DedicatedServer.this).start();
++ /*
+ jline.console.ConsoleReader bufferedreader = DedicatedServer.this.reader;
+
+ // MC-33041, SPIGOT-5538: if System.in is not valid due to javaw, then return
+@@ -142,7 +145,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ continue;
+ }
+ if (s.trim().length() > 0) { // Trim to filter lines which are just spaces
+- DedicatedServer.this.handleConsoleInput(s, DedicatedServer.this.createCommandSourceStack());
++ DedicatedServer.this.issueCommand(s, DedicatedServer.this.getServerCommandListener());
+ }
+ // CraftBukkit end
+ }
+@@ -150,6 +153,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ DedicatedServer.LOGGER.error("Exception handling console input", ioexception);
+ }
+
++ */
++ // Paper end
+ }
+ };
+
+@@ -161,6 +166,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ }
+ global.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
+
++ // Paper start - Not needed with TerminalConsoleAppender
++ final org.apache.logging.log4j.Logger logger = LogManager.getRootLogger();
++ /*
+ final org.apache.logging.log4j.core.Logger logger = ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger());
+ for (org.apache.logging.log4j.core.Appender appender : logger.getAppenders().values()) {
+ if (appender instanceof org.apache.logging.log4j.core.appender.ConsoleAppender) {
+@@ -171,6 +179,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ TerminalConsoleWriterThread writerThread = new TerminalConsoleWriterThread(System.out, this.reader);
+ this.reader.setCompletionHandler(new TerminalCompletionHandler(writerThread, this.reader.getCompletionHandler()));
+ writerThread.start();
++ */
++ // Paper end - Not needed with TerminalConsoleAppender
+
+ System.setOut(IoBuilder.forLogger(logger).setLevel(Level.INFO).buildPrintStream());
+ System.setErr(IoBuilder.forLogger(logger).setLevel(Level.WARN).buildPrintStream());
+diff --git a/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java b/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java
+index 3d92c61f7781221cfdc0324d11bd0088954e4a68..84a2c6c397604279ba821286f5c3c855e6041400 100644
+--- a/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java
++++ b/src/main/java/net/minecraft/server/gui/MinecraftServerGui.java
+@@ -166,7 +166,7 @@ public class MinecraftServerGui extends JComponent {
+ this.finalizers.forEach(Runnable::run);
+ }
+
+- private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]"); // CraftBukkit
++ private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper
+ public void print(JTextArea textArea, JScrollPane scrollPane, String message) {
+ if (!SwingUtilities.isEventDispatchThread()) {
+ SwingUtilities.invokeLater(() -> {
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 1333daa8666fe2ec4033a2f57ba6b716fcdd5343..8daa027a94602d7d556cf4fbfc8fcd97caf6bd98 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -162,8 +162,7 @@ public abstract class PlayerList {
+
+ public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registryManager, PlayerDataStorage saveHandler, int maxPlayers) {
+ this.cserver = server.server = new CraftServer((DedicatedServer) server, this);
+- server.console = org.bukkit.craftbukkit.command.ColouredConsoleSender.getInstance();
+- server.reader.addCompleter(new org.bukkit.craftbukkit.command.ConsoleCommandCompleter(server.server));
++ server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper
+ // CraftBukkit end
+
+ this.bans = new UserBanList(PlayerList.USERBANLIST_FILE);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index c3774d9a253d4fda80f63d4040722ab5c1c94be4..41aa22f431c989d60dde5c85ca2821d5bcf613af 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -43,7 +43,7 @@ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import java.util.stream.Collectors;
+ import javax.imageio.ImageIO;
+-import jline.console.ConsoleReader;
++// import jline.console.ConsoleReader;
+ import net.minecraft.advancements.AdvancementHolder;
+ import net.minecraft.commands.CommandSourceStack;
+ import net.minecraft.commands.Commands;
+@@ -1359,9 +1359,13 @@ public final class CraftServer implements Server {
+ return this.logger;
+ }
+
++ // Paper start - JLine update
++ /*
+ public ConsoleReader getReader() {
+ return this.console.reader;
+ }
++ */
++ // Paper end
+
+ @Override
+ public PluginCommand getPluginCommand(String name) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index c210d21382e0922aaf61f2c51949f753e6462b9e..1f4dc832ac059ddb9eafd43b0a37436abadaa59f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -13,7 +13,6 @@ import java.util.logging.Logger;
+ import joptsimple.OptionParser;
+ import joptsimple.OptionSet;
+ import joptsimple.util.PathConverter;
+-import org.fusesource.jansi.AnsiConsole;
+
+ public class Main {
+ public static boolean useJline = true;
+@@ -196,6 +195,8 @@ public class Main {
+ }
+
+ try {
++ // Paper start - Handled by TerminalConsoleAppender
++ /*
+ // This trick bypasses Maven Shade's clever rewriting of our getProperty call when using String literals
+ String jline_UnsupportedTerminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 'U', 'n', 's', 'u', 'p', 'p', 'o', 'r', 't', 'e', 'd', 'T', 'e', 'r', 'm', 'i', 'n', 'a', 'l'});
+ String jline_terminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l'});
+@@ -213,9 +214,18 @@ public class Main {
+ // This ensures the terminal literal will always match the jline implementation
+ System.setProperty(jline.TerminalFactory.JLINE_TERMINAL, jline.UnsupportedTerminal.class.getName());
+ }
++ */
++
++ if (options.has("nojline")) {
++ System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false");
++ useJline = false;
++ }
++ // Paper end
+
+ if (options.has("noconsole")) {
+ Main.useConsole = false;
++ useJline = false; // Paper
++ System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper
+ }
+
+ if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) {
+@@ -231,6 +241,8 @@ public class Main {
+ }
+ }
+
++ System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
++ System.setProperty("jdk.console", "java.base"); // Paper - revert default console provider back to java.base so we can have our own jline
+ System.out.println("Loading libraries, please wait...");
+ net.minecraft.server.Main.main(options);
+ } catch (Throwable t) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java b/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java
+index bcf1c36d07b79520a39643d3a01020a67b1c9ef2..217e7e3b9db04c7fc5f6518f39cc9d3488f9128d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java
+@@ -5,15 +5,13 @@ import java.util.EnumMap;
+ import java.util.Map;
+ import java.util.regex.Matcher;
+ import java.util.regex.Pattern;
+-import jline.Terminal;
++//import jline.Terminal;
+ import org.bukkit.Bukkit;
+ import org.bukkit.ChatColor;
+ import org.bukkit.command.ConsoleCommandSender;
+ import org.bukkit.craftbukkit.CraftServer;
+-import org.fusesource.jansi.Ansi;
+-import org.fusesource.jansi.Ansi.Attribute;
+
+-public class ColouredConsoleSender extends CraftConsoleCommandSender {
++public class ColouredConsoleSender /*extends CraftConsoleCommandSender */{/* // Paper - disable
+ private final Terminal terminal;
+ private final Map<ChatColor, String> replacements = new EnumMap<ChatColor, String>(ChatColor.class);
+ private final ChatColor[] colors = ChatColor.values();
+@@ -93,5 +91,5 @@ public class ColouredConsoleSender extends CraftConsoleCommandSender {
+ } else {
+ return new ColouredConsoleSender();
+ }
+- }
++ }*/ // Paper
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
+index 0b4c62387c1093652ac15b64a8703249de4cf088..0b27172073530617a6e0b2b83fe1e9d231653b8d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
++++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
+@@ -4,50 +4,73 @@ import java.util.Collections;
+ import java.util.List;
+ import java.util.concurrent.ExecutionException;
+ import java.util.logging.Level;
+-import jline.console.completer.Completer;
++import net.minecraft.server.dedicated.DedicatedServer;
+ import org.bukkit.craftbukkit.CraftServer;
+ import org.bukkit.craftbukkit.util.Waitable;
++
++// Paper start - JLine update
++import org.jline.reader.Candidate;
++import org.jline.reader.Completer;
++import org.jline.reader.LineReader;
++import org.jline.reader.ParsedLine;
++// Paper end
+ import org.bukkit.event.server.TabCompleteEvent;
+
+ public class ConsoleCommandCompleter implements Completer {
+- private final CraftServer server;
++ private final DedicatedServer server; // Paper - CraftServer -> DedicatedServer
+
+- public ConsoleCommandCompleter(CraftServer server) {
++ public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer
+ this.server = server;
+ }
+
++ // Paper start - Change method signature for JLine update
+ @Override
+- public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) {
++ public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
++ final CraftServer server = this.server.server;
++ final String buffer = "/" + line.line();
++ // Paper end
+ Waitable<List<String>> waitable = new Waitable<List<String>>() {
+ @Override
+ protected List<String> evaluate() {
+- List<String> offers = ConsoleCommandCompleter.this.server.getCommandMap().tabComplete(ConsoleCommandCompleter.this.server.getConsoleSender(), buffer);
++ List<String> offers = server.getCommandMap().tabComplete(server.getConsoleSender(), buffer); // Paper - Remove "this."
+
+- TabCompleteEvent tabEvent = new TabCompleteEvent(ConsoleCommandCompleter.this.server.getConsoleSender(), buffer, (offers == null) ? Collections.EMPTY_LIST : offers);
+- ConsoleCommandCompleter.this.server.getPluginManager().callEvent(tabEvent);
++ TabCompleteEvent tabEvent = new TabCompleteEvent(server.getConsoleSender(), buffer, (offers == null) ? Collections.EMPTY_LIST : offers); // Paper - Remove "this."
++ server.getPluginManager().callEvent(tabEvent); // Paper - Remove "this."
+
+ return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions();
+ }
+ };
+- this.server.getServer().processQueue.add(waitable);
++ server.getServer().processQueue.add(waitable); // Paper - Remove "this."
+ try {
+ List<String> offers = waitable.get();
+ if (offers == null) {
+- return cursor;
++ return; // Paper - Method returns void
++ }
++
++ // Paper start - JLine update
++ for (String completion : offers) {
++ if (completion.isEmpty()) {
++ continue;
++ }
++
++ candidates.add(new Candidate(completion));
+ }
+- candidates.addAll(offers);
++ // Paper end
+
++ // Paper start - JLine handles cursor now
++ /*
+ final int lastSpace = buffer.lastIndexOf(' ');
+ if (lastSpace == -1) {
+ return cursor - buffer.length();
+ } else {
+ return cursor - (buffer.length() - lastSpace - 1);
+ }
++ */
++ // Paper end
+ } catch (ExecutionException e) {
+- this.server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e);
++ server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); // Paper - Remove "this."
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+- return cursor;
+ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
+index 8390f5b5b957b5435efece26507a89756d0a7b3c..c6e8441e299f477ddb22c1ce2618710763978f1a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
+@@ -16,7 +16,7 @@ public class ServerShutdownThread extends Thread {
+ this.server.close();
+ } finally {
+ try {
+- this.server.reader.getTerminal().restore();
++ net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
+ } catch (Exception e) {
+ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java b/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java
+index 7d1c0810181c51ffa64f9a23b251e05c789870b0..09804513265e6db0c1f3501355335551100a36a8 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/TerminalCompletionHandler.java
+@@ -4,14 +4,12 @@ import java.io.IOException;
+ import java.util.HashSet;
+ import java.util.List;
+ import java.util.Set;
+-import jline.console.ConsoleReader;
+-import jline.console.completer.CompletionHandler;
+
+ /**
+ * SPIGOT-6705: Make sure we print the display line again on tab completion, so that the user does not get stuck on it
+ * e.g. The user needs to press y / n to continue
+ */
+-public class TerminalCompletionHandler implements CompletionHandler {
++public class TerminalCompletionHandler /* implements CompletionHandler */ { /* Paper - comment out whole class
+
+ private final TerminalConsoleWriterThread writerThread;
+ private final CompletionHandler delegate;
+@@ -50,4 +48,5 @@ public class TerminalCompletionHandler implements CompletionHandler {
+
+ return result;
+ }
++*/ // Paper end - comment out whole class
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java b/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java
+index 2e5101fca081548bb616bc5eaa0134698719b63a..6e4d50a4863d43a2d6a2d574350c74d0819df402 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java
+@@ -7,13 +7,9 @@ import java.util.Locale;
+ import java.util.ResourceBundle;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+-import jline.console.ConsoleReader;
+-import jline.console.completer.CandidateListCompletionHandler;
+ import org.bukkit.craftbukkit.Main;
+-import org.fusesource.jansi.Ansi;
+-import org.fusesource.jansi.Ansi.Erase;
+
+-public class TerminalConsoleWriterThread extends Thread {
++public class TerminalConsoleWriterThread /*extends Thread*/ {/* // Paper - Comment out entire class
+ private final ResourceBundle bundle = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault());
+ private final ConsoleReader reader;
+ private final OutputStream output;
+@@ -70,4 +66,5 @@ public class TerminalConsoleWriterThread extends Thread {
+ void setCompletion(int completion) {
+ this.completion = completion;
+ }
++*/ // Paper - Comment out entire class
+ }
+diff --git a/src/main/resources/log4j2.component.properties b/src/main/resources/log4j2.component.properties
+new file mode 100644
+index 0000000000000000000000000000000000000000..0694b21465fb9e4164e71862ff24b62241b191f2
+--- /dev/null
++++ b/src/main/resources/log4j2.component.properties
+@@ -0,0 +1 @@
++log4j.skipJansi=true
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index 0ff3f750fb7f684c12080894cd8660f0455c658b..301874c1fe16c52ffa6228d79e6617d746e9a035 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -1,17 +1,14 @@
+ <?xml version="1.0" encoding="UTF-8"?>
+ <Configuration status="WARN">
+ <Appenders>
+- <Console name="SysOut" target="SYSTEM_OUT">
+- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
+- </Console>
+ <Queue name="ServerGuiConsole">
+ <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
+ </Queue>
+- <Queue name="TerminalConsole">
+- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
+- </Queue>
++ <TerminalConsole name="TerminalConsole">
++ <PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" />
++ </TerminalConsole>
+ <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
+- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
++ <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" />
+ <Policies>
+ <TimeBasedTriggeringPolicy />
+ <OnStartupTriggeringPolicy />
+@@ -24,10 +21,9 @@
+ <filters>
+ <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
+ </filters>
+- <AppenderRef ref="SysOut" level="info"/>
+ <AppenderRef ref="File"/>
+- <AppenderRef ref="ServerGuiConsole" level="info"/>
+ <AppenderRef ref="TerminalConsole" level="info"/>
++ <AppenderRef ref="ServerGuiConsole" level="info"/>
+ </Root>
+ </Loggers>
+ </Configuration>
diff --git a/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch b/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch
new file mode 100644
index 0000000000..a508959733
--- /dev/null
+++ b/patches/server/0012-Handle-plugin-prefixes-using-Log4J-configuration.patch
@@ -0,0 +1,71 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Minecrell <[email protected]>
+Date: Thu, 21 Sep 2017 16:14:55 +0200
+Subject: [PATCH] Handle plugin prefixes using Log4J configuration
+
+Display logger name in the console for all loggers except the
+root logger, Bukkit's logger ("Minecraft") and Minecraft loggers.
+Since plugins now use the plugin name as logger name this will
+restore the plugin prefixes without having to prepend them manually
+to the log messages.
+
+Logger prefixes are shown by default for all loggers except for
+the root logger, the Minecraft/Mojang loggers and the Bukkit loggers.
+This may cause additional prefixes to be disabled for plugins bypassing
+the plugin logger.
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 611960836f7f66af17e9a231d85e7181dcc068c8..dca5a9897195315a1e2c006aa3ae4338742e3fc9 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -36,7 +36,7 @@ dependencies {
+ all its classes to check if they are plugins.
+ Scanning takes about 1-2 seconds so adding this speeds up the server start.
+ */
+- runtimeOnly("org.apache.logging.log4j:log4j-core:2.19.0")
++ implementation("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - implementation
+ log4jPlugins.annotationProcessorConfigurationName("org.apache.logging.log4j:log4j-core:2.19.0") // Paper - Needed to generate meta for our Log4j plugins
+ runtimeOnly(log4jPlugins.output)
+ alsoShade(log4jPlugins.output)
+diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
+index e42677a14ec8e1a42747603fb4112822e326fb70..744edd40128c910c3ad2f3657bde995612e0a1e4 100644
+--- a/src/main/java/org/spigotmc/SpigotConfig.java
++++ b/src/main/java/org/spigotmc/SpigotConfig.java
+@@ -284,7 +284,7 @@ public class SpigotConfig
+ private static void playerSample()
+ {
+ SpigotConfig.playerSample = SpigotConfig.getInt( "settings.sample-count", 12 );
+- System.out.println( "Server Ping Player Sample Count: " + SpigotConfig.playerSample );
++ Bukkit.getLogger().log( Level.INFO, "Server Ping Player Sample Count: {0}", playerSample ); // Paper - Use logger
+ }
+
+ public static int playerShuffle;
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index 301874c1fe16c52ffa6228d79e6617d746e9a035..e073707a46397f62bedf1d413f9e5764e77dda6a 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -5,10 +5,22 @@
+ <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
+ </Queue>
+ <TerminalConsole name="TerminalConsole">
+- <PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" />
++ <PatternLayout>
++ <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx}">
++ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang."
++ pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" />
++ </LoggerNamePatternSelector>
++ </PatternLayout>
+ </TerminalConsole>
+ <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
+- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" />
++ <PatternLayout>
++ <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n">
++ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang."
++ pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" />
++ </LoggerNamePatternSelector>
++ </PatternLayout>
+ <Policies>
+ <TimeBasedTriggeringPolicy />
+ <OnStartupTriggeringPolicy />
diff --git a/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch b/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch
new file mode 100644
index 0000000000..4f654facd6
--- /dev/null
+++ b/patches/server/0013-Improve-Log4J-Configuration-Plugin-Loggers.patch
@@ -0,0 +1,47 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Minecrell <[email protected]>
+Date: Sat, 23 Sep 2017 21:07:20 +0200
+Subject: [PATCH] Improve Log4J Configuration / Plugin Loggers
+
+Add full exceptions to log4j to not truncate stack traces
+
+Disable logger prefix for various plugins bypassing the plugin logger
+
+Some plugins bypass the plugin logger and add the plugin prefix
+manually to the log message. Since they use other logger names
+(e.g. qualified class names) these would now also appear in the
+log. Disable the logger prefix for these plugins so the messages
+show up correctly.
+
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index e073707a46397f62bedf1d413f9e5764e77dda6a..ab1caec640128aa90f246e4bbecf5ca275e7982e 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -6,19 +6,21 @@
+ </Queue>
+ <TerminalConsole name="TerminalConsole">
+ <PatternLayout>
+- <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx}">
++ <LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx{full}}">
+ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
+- <PatternMatch key=",net.minecraft.,Minecraft,com.mojang."
+- pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" />
++ <!-- Disable prefix for various plugins that bypass the plugin logger -->
++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE"
++ pattern="%highlightError{[%d{HH:mm:ss} %level]: %msg%n%xEx{full}}" />
+ </LoggerNamePatternSelector>
+ </PatternLayout>
+ </TerminalConsole>
+ <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
+ <PatternLayout>
+- <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n">
++ <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n%xEx{full}">
+ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
+- <PatternMatch key=",net.minecraft.,Minecraft,com.mojang."
+- pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n" />
++ <!-- Disable prefix for various plugins that bypass the plugin logger -->
++ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE"
++ pattern="[%d{HH:mm:ss}] [%t/%level]: %stripAnsi{%msg}%n%xEx{full}" />
+ </LoggerNamePatternSelector>
+ </PatternLayout>
+ <Policies>
diff --git a/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch b/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch
new file mode 100644
index 0000000000..8ca83419cc
--- /dev/null
+++ b/patches/server/0014-Use-AsyncAppender-to-keep-logging-IO-off-main-thread.patch
@@ -0,0 +1,44 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Thu, 12 Aug 2021 04:46:41 -0700
+Subject: [PATCH] Use AsyncAppender to keep logging IO off main thread
+
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index dca5a9897195315a1e2c006aa3ae4338742e3fc9..ca7d5e2bb44e2719eee8ad046e40e1e52021a4a9 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -47,6 +47,7 @@ dependencies {
+ implementation("commons-lang:commons-lang:2.6")
+ runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
+ runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
++ runtimeOnly("com.lmax:disruptor:3.4.4") // Paper
+
+ runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6")
+ runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18")
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index ab1caec640128aa90f246e4bbecf5ca275e7982e..18e961a37b2830da6e5dab7aa35116b2f5215898 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -29,15 +29,18 @@
+ </Policies>
+ <DefaultRolloverStrategy max="1000"/>
+ </RollingRandomAccessFile>
++ <Async name="Async">
++ <AppenderRef ref="File"/>
++ <AppenderRef ref="TerminalConsole" level="info"/>
++ <AppenderRef ref="ServerGuiConsole" level="info"/>
++ </Async>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <filters>
+ <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
+ </filters>
+- <AppenderRef ref="File"/>
+- <AppenderRef ref="TerminalConsole" level="info"/>
+- <AppenderRef ref="ServerGuiConsole" level="info"/>
++ <AppenderRef ref="Async"/>
+ </Root>
+ </Loggers>
+ </Configuration>
diff --git a/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch
new file mode 100644
index 0000000000..8160890afe
--- /dev/null
+++ b/patches/server/0015-Deobfuscate-stacktraces-in-log-messages-crash-report.patch
@@ -0,0 +1,581 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Sun, 20 Jun 2021 18:19:09 -0700
+Subject: [PATCH] Deobfuscate stacktraces in log messages, crash reports, and
+ etc.
+
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index ca7d5e2bb44e2719eee8ad046e40e1e52021a4a9..19d9cbcaa05061a5bedf5b1d821138091acfe973 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -60,6 +60,7 @@ dependencies {
+ mockitoAgent("org.mockito:mockito-core:5.14.1") { isTransitive = false } // Paper - configure mockito agent that is needed in newer java versions
+ testImplementation("org.ow2.asm:asm-tree:9.7.1")
+ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
++ implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
+ }
+
+ paperweight {
+diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..66b6011ee3684695b2ab9292961c80bf2a420ee9
+--- /dev/null
++++ b/src/log4jPlugins/java/io/papermc/paper/logging/StacktraceDeobfuscatingRewritePolicy.java
+@@ -0,0 +1,66 @@
++package io.papermc.paper.logging;
++
++import java.lang.invoke.MethodHandle;
++import java.lang.invoke.MethodHandles;
++import java.lang.invoke.VarHandle;
++import org.apache.logging.log4j.core.Core;
++import org.apache.logging.log4j.core.LogEvent;
++import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
++import org.apache.logging.log4j.core.config.plugins.Plugin;
++import org.apache.logging.log4j.core.config.plugins.PluginFactory;
++import org.apache.logging.log4j.core.impl.Log4jLogEvent;
++import org.checkerframework.checker.nullness.qual.NonNull;
++
++@Plugin(
++ name = "StacktraceDeobfuscatingRewritePolicy",
++ category = Core.CATEGORY_NAME,
++ elementType = "rewritePolicy",
++ printObject = true
++)
++public final class StacktraceDeobfuscatingRewritePolicy implements RewritePolicy {
++ private static final MethodHandle DEOBFUSCATE_THROWABLE;
++
++ static {
++ try {
++ final Class<?> cls = Class.forName("io.papermc.paper.util.StacktraceDeobfuscator");
++ final MethodHandles.Lookup lookup = MethodHandles.lookup();
++ final VarHandle instanceHandle = lookup.findStaticVarHandle(cls, "INSTANCE", cls);
++ final Object deobfuscator = instanceHandle.get();
++ DEOBFUSCATE_THROWABLE = lookup
++ .unreflect(cls.getDeclaredMethod("deobfuscateThrowable", Throwable.class))
++ .bindTo(deobfuscator);
++ } catch (final ReflectiveOperationException ex) {
++ throw new IllegalStateException(ex);
++ }
++ }
++
++ private StacktraceDeobfuscatingRewritePolicy() {
++ }
++
++ @Override
++ public @NonNull LogEvent rewrite(final @NonNull LogEvent rewrite) {
++ final Throwable thrown = rewrite.getThrown();
++ if (thrown != null) {
++ deobfuscateThrowable(thrown);
++ return new Log4jLogEvent.Builder(rewrite)
++ .setThrownProxy(null)
++ .build();
++ }
++ return rewrite;
++ }
++
++ private static void deobfuscateThrowable(final Throwable thrown) {
++ try {
++ DEOBFUSCATE_THROWABLE.invoke(thrown);
++ } catch (final Error e) {
++ throw e;
++ } catch (final Throwable e) {
++ throw new RuntimeException(e);
++ }
++ }
++
++ @PluginFactory
++ public static @NonNull StacktraceDeobfuscatingRewritePolicy createPolicy() {
++ return new StacktraceDeobfuscatingRewritePolicy();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/ObfHelper.java b/src/main/java/io/papermc/paper/util/ObfHelper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9e6d48335b37fa5204bfebf396d748089884555b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/ObfHelper.java
+@@ -0,0 +1,156 @@
++package io.papermc.paper.util;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Map;
++import java.util.Objects;
++import java.util.Set;
++import java.util.stream.Collectors;
++import net.neoforged.srgutils.IMappingFile;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public enum ObfHelper {
++ INSTANCE;
++
++ private final @Nullable Map<String, ClassMapping> mappingsByObfName;
++ private final @Nullable Map<String, ClassMapping> mappingsByMojangName;
++
++ ObfHelper() {
++ final @Nullable Set<ClassMapping> maps = loadMappingsIfPresent();
++ if (maps != null) {
++ this.mappingsByObfName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::obfName, map -> map));
++ this.mappingsByMojangName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::mojangName, map -> map));
++ } else {
++ this.mappingsByObfName = null;
++ this.mappingsByMojangName = null;
++ }
++ }
++
++ public @Nullable Map<String, ClassMapping> mappingsByObfName() {
++ return this.mappingsByObfName;
++ }
++
++ public @Nullable Map<String, ClassMapping> mappingsByMojangName() {
++ return this.mappingsByMojangName;
++ }
++
++ /**
++ * Attempts to get the obf name for a given class by its Mojang name. Will
++ * return the input string if mappings are not present.
++ *
++ * @param fullyQualifiedMojangName fully qualified class name (dotted)
++ * @return mapped or original fully qualified (dotted) class name
++ */
++ public String reobfClassName(final String fullyQualifiedMojangName) {
++ if (this.mappingsByMojangName == null) {
++ return fullyQualifiedMojangName;
++ }
++
++ final ClassMapping map = this.mappingsByMojangName.get(fullyQualifiedMojangName);
++ if (map == null) {
++ return fullyQualifiedMojangName;
++ }
++
++ return map.obfName();
++ }
++
++ /**
++ * Attempts to get the Mojang name for a given class by its obf name. Will
++ * return the input string if mappings are not present.
++ *
++ * @param fullyQualifiedObfName fully qualified class name (dotted)
++ * @return mapped or original fully qualified (dotted) class name
++ */
++ public String deobfClassName(final String fullyQualifiedObfName) {
++ if (this.mappingsByObfName == null) {
++ return fullyQualifiedObfName;
++ }
++
++ final ClassMapping map = this.mappingsByObfName.get(fullyQualifiedObfName);
++ if (map == null) {
++ return fullyQualifiedObfName;
++ }
++
++ return map.mojangName();
++ }
++
++ private static @Nullable Set<ClassMapping> loadMappingsIfPresent() {
++ try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
++ if (mappingsInputStream == null) {
++ return null;
++ }
++ final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot
++ final Set<ClassMapping> classes = new HashSet<>();
++
++ final StringPool pool = new StringPool();
++ for (final IMappingFile.IClass cls : mappings.getClasses()) {
++ final Map<String, String> methods = new HashMap<>();
++ final Map<String, String> fields = new HashMap<>();
++ final Map<String, String> strippedMethods = new HashMap<>();
++
++ for (final IMappingFile.IMethod methodMapping : cls.getMethods()) {
++ methods.put(
++ pool.string(methodKey(
++ Objects.requireNonNull(methodMapping.getMapped()),
++ Objects.requireNonNull(methodMapping.getMappedDescriptor())
++ )),
++ pool.string(Objects.requireNonNull(methodMapping.getOriginal()))
++ );
++
++ strippedMethods.put(
++ pool.string(pool.string(strippedMethodKey(
++ methodMapping.getMapped(),
++ methodMapping.getDescriptor()
++ ))),
++ pool.string(methodMapping.getOriginal())
++ );
++ }
++ for (final IMappingFile.IField field : cls.getFields()) {
++ fields.put(
++ pool.string(field.getMapped()),
++ pool.string(field.getOriginal())
++ );
++ }
++
++ final ClassMapping map = new ClassMapping(
++ Objects.requireNonNull(cls.getMapped()).replace('/', '.'),
++ Objects.requireNonNull(cls.getOriginal()).replace('/', '.'),
++ Map.copyOf(methods),
++ Map.copyOf(fields),
++ Map.copyOf(strippedMethods)
++ );
++ classes.add(map);
++ }
++
++ return Set.copyOf(classes);
++ } catch (final IOException ex) {
++ System.err.println("Failed to load mappings.");
++ ex.printStackTrace();
++ return null;
++ }
++ }
++
++ public static String strippedMethodKey(final String methodName, final String methodDescriptor) {
++ final String methodKey = methodKey(methodName, methodDescriptor);
++ final int returnDescriptorEnd = methodKey.indexOf(')');
++ return methodKey.substring(0, returnDescriptorEnd + 1);
++ }
++
++ public static String methodKey(final String methodName, final String methodDescriptor) {
++ return methodName + methodDescriptor;
++ }
++
++ public record ClassMapping(
++ String obfName,
++ String mojangName,
++ Map<String, String> methodsByObf,
++ Map<String, String> fieldsByObf,
++ // obf name with mapped desc to mapped name. return value is excluded from desc as reflection doesn't use it
++ Map<String, String> strippedMethods
++ ) {}
++}
+diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..242811578a786e3807a1a7019d472d5a68f87116
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
+@@ -0,0 +1,144 @@
++package io.papermc.paper.util;
++
++import io.papermc.paper.configuration.GlobalConfiguration;
++import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
++import java.io.IOException;
++import java.io.InputStream;
++import java.util.Collections;
++import java.util.LinkedHashMap;
++import java.util.Map;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.objectweb.asm.ClassReader;
++import org.objectweb.asm.ClassVisitor;
++import org.objectweb.asm.Label;
++import org.objectweb.asm.MethodVisitor;
++import org.objectweb.asm.Opcodes;
++
++@DefaultQualifier(NonNull.class)
++public enum StacktraceDeobfuscator {
++ INSTANCE;
++
++ private final Map<Class<?>, Int2ObjectMap<String>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) {
++ @Override
++ protected boolean removeEldestEntry(final Map.Entry<Class<?>, Int2ObjectMap<String>> eldest) {
++ return this.size() > 127;
++ }
++ });
++
++ public void deobfuscateThrowable(final Throwable throwable) {
++ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
++ return;
++ }
++
++ throwable.setStackTrace(this.deobfuscateStacktrace(throwable.getStackTrace()));
++ final Throwable cause = throwable.getCause();
++ if (cause != null) {
++ this.deobfuscateThrowable(cause);
++ }
++ for (final Throwable suppressed : throwable.getSuppressed()) {
++ this.deobfuscateThrowable(suppressed);
++ }
++ }
++
++ public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) {
++ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
++ return traceElements;
++ }
++
++ final @Nullable Map<String, ObfHelper.ClassMapping> mappings = ObfHelper.INSTANCE.mappingsByObfName();
++ if (mappings == null || traceElements.length == 0) {
++ return traceElements;
++ }
++ final StackTraceElement[] result = new StackTraceElement[traceElements.length];
++ for (int i = 0; i < traceElements.length; i++) {
++ final StackTraceElement element = traceElements[i];
++
++ final String className = element.getClassName();
++ final String methodName = element.getMethodName();
++
++ final ObfHelper.ClassMapping classMapping = mappings.get(className);
++ if (classMapping == null) {
++ result[i] = element;
++ continue;
++ }
++
++ final Class<?> clazz;
++ try {
++ clazz = Class.forName(className);
++ } catch (final ClassNotFoundException ex) {
++ throw new RuntimeException(ex);
++ }
++ final @Nullable String methodKey = this.determineMethodForLine(clazz, element.getLineNumber());
++ final @Nullable String mappedMethodName = methodKey == null ? null : classMapping.methodsByObf().get(methodKey);
++
++ result[i] = new StackTraceElement(
++ element.getClassLoaderName(),
++ element.getModuleName(),
++ element.getModuleVersion(),
++ classMapping.mojangName(),
++ mappedMethodName != null ? mappedMethodName : methodName,
++ sourceFileName(classMapping.mojangName()),
++ element.getLineNumber()
++ );
++ }
++ return result;
++ }
++
++ private @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) {
++ return this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap).get(lineNumber);
++ }
++
++ private static String sourceFileName(final String fullClassName) {
++ final int dot = fullClassName.lastIndexOf('.');
++ final String className = dot == -1
++ ? fullClassName
++ : fullClassName.substring(dot + 1);
++ final String rootClassName = className.split("\\$")[0];
++ return rootClassName + ".java";
++ }
++
++ private static Int2ObjectMap<String> buildLineMap(final Class<?> key) {
++ final StringPool pool = new StringPool();
++ final Int2ObjectMap<String> lineMap = new Int2ObjectOpenHashMap<>();
++ final class LineCollectingMethodVisitor extends MethodVisitor {
++ private final String name;
++ private final String descriptor;
++
++ LineCollectingMethodVisitor(final String name, final String descriptor) {
++ super(Opcodes.ASM9);
++ this.name = name;
++ this.descriptor = descriptor;
++ }
++
++ @Override
++ public void visitLineNumber(final int line, final Label start) {
++ lineMap.put(line, pool.string(ObfHelper.methodKey(this.name, this.descriptor)));
++ }
++ }
++ final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
++ @Override
++ public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
++ return new LineCollectingMethodVisitor(name, descriptor);
++ }
++ };
++ try {
++ final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader()
++ .getResourceAsStream(key.getName().replace('.', '/') + ".class");
++ if (inputStream == null) {
++ throw new IllegalStateException("Could not find class file: " + key.getName());
++ }
++ final byte[] classData;
++ try (inputStream) {
++ classData = inputStream.readAllBytes();
++ }
++ final ClassReader reader = new ClassReader(classData);
++ reader.accept(classVisitor, 0);
++ } catch (final IOException ex) {
++ throw new RuntimeException(ex);
++ }
++ return lineMap;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/StringPool.java b/src/main/java/io/papermc/paper/util/StringPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c0a486cb46ff30353c3ff09567891cd36238eeb4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/StringPool.java
+@@ -0,0 +1,34 @@
++package io.papermc.paper.util;
++
++import java.util.HashMap;
++import java.util.Map;
++import java.util.function.Function;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++/**
++ * De-duplicates {@link String} instances without using {@link String#intern()}.
++ *
++ * <p>Interning may not be desired as we may want to use the heap for our pool,
++ * so it can be garbage collected as normal, etc.</p>
++ *
++ * <p>Additionally, interning can be slow due to the potentially large size of the
++ * pool (as it is shared for the entire JVM), and because most JVMs implement
++ * it using JNI.</p>
++ */
++@DefaultQualifier(NonNull.class)
++public final class StringPool {
++ private final Map<String, String> pool;
++
++ public StringPool() {
++ this(new HashMap<>());
++ }
++
++ public StringPool(final Map<String, String> map) {
++ this.pool = map;
++ }
++
++ public String string(final String string) {
++ return this.pool.computeIfAbsent(string, Function.identity());
++ }
++}
+diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java
+index 1938ae691dafec1fc1e5a68792d1191bd52b4e5c..268310642181a715815d3b2d1c0f090e6252971a 100644
+--- a/src/main/java/net/minecraft/CrashReport.java
++++ b/src/main/java/net/minecraft/CrashReport.java
+@@ -34,6 +34,7 @@ public class CrashReport {
+ private final SystemReport systemReport = new SystemReport();
+
+ public CrashReport(String message, Throwable cause) {
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper
+ this.title = message;
+ this.exception = cause;
+ this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit
+diff --git a/src/main/java/net/minecraft/CrashReportCategory.java b/src/main/java/net/minecraft/CrashReportCategory.java
+index 81831a061d7fbf513f4aa7880e3b18ef3fdc05d7..1e9873d7b258ce1f0b2437cb1e487157a16f6834 100644
+--- a/src/main/java/net/minecraft/CrashReportCategory.java
++++ b/src/main/java/net/minecraft/CrashReportCategory.java
+@@ -110,6 +110,7 @@ public class CrashReportCategory {
+ } else {
+ this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount];
+ System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length);
++ this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper
+ return this.stackTrace.length;
+ }
+ }
+diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
+index 1fc859f4cc1cf552d2d578b3cda5872bc8b1015a..00bf34e1fd3593ad6d92bd292f3069cd3cbddfdd 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -82,13 +82,13 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+ marker.add(Connection.PACKET_MARKER);
+ });
+ public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build());
++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build());
++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(() -> {
+- return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build());
++ return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
+ private final PacketFlow receiving;
+@@ -197,7 +197,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+
+ }
+ }
+- if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot
++ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
+ }
+
+ protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) {
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index dcb9258d3fbdfdfd41065d4c0919ed4300eac3ae..a61a92078a8bb4979f231c02ef5aa990b8ab57ad 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -209,6 +209,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
+ org.spigotmc.SpigotConfig.registerCommands();
+ // Spigot end
++ io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc.
+ // Paper start - initialize global and world-defaults configuration
+ this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess());
+ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
+diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
+index 987360a266a5a870f06929b00c9f451901188fd6..2cf3e79ec5e8706b71d27ebad4668773f0b91195 100644
+--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
+@@ -52,10 +52,10 @@ public class ServerConnectionListener {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ public static final Supplier<NioEventLoopGroup> SERVER_EVENT_GROUP = Suppliers.memoize(() -> {
+- return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build());
++ return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ public static final Supplier<EpollEventLoopGroup> SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> {
+- return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build());
++ return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+ });
+ final MinecraftServer server;
+ public volatile boolean running;
+diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+index 8d06e8d286da2573e40794adab695ff77e5afd86..68551947f5b7d3471f15bd74ccd86519ab34c1c1 100644
+--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
++++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+@@ -356,7 +356,7 @@ public class OldUsersConverter {
+ try {
+ root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap());
+ } catch (Exception exception) {
+- exception.printStackTrace();
++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
+ }
+
+ if (root != null) {
+@@ -369,7 +369,7 @@ public class OldUsersConverter {
+ try {
+ NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
+ } catch (Exception exception) {
+- exception.printStackTrace();
++ io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
+ }
+ }
+ // CraftBukkit end
+diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
+index c4bf7053d83d207caca0e13e19f5c1afa7062de3..f697d45e0ac4e9cdc8a46121510a04c0f294d91f 100644
+--- a/src/main/java/org/spigotmc/WatchdogThread.java
++++ b/src/main/java/org/spigotmc/WatchdogThread.java
+@@ -130,7 +130,7 @@ public class WatchdogThread extends Thread
+ }
+ log.log( Level.SEVERE, "\tStack:" );
+ //
+- for ( StackTraceElement stack : thread.getStackTrace() )
++ for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper
+ {
+ log.log( Level.SEVERE, "\t\t" + stack );
+ }
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index 18e961a37b2830da6e5dab7aa35116b2f5215898..128fa1376f22d3429a23d79a2772abf2e7fec2bc 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -30,10 +30,14 @@
+ <DefaultRolloverStrategy max="1000"/>
+ </RollingRandomAccessFile>
+ <Async name="Async">
++ <AppenderRef ref="rewrite"/>
++ </Async>
++ <Rewrite name="rewrite">
++ <StacktraceDeobfuscatingRewritePolicy />
+ <AppenderRef ref="File"/>
+ <AppenderRef ref="TerminalConsole" level="info"/>
+ <AppenderRef ref="ServerGuiConsole" level="info"/>
+- </Async>
++ </Rewrite>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
diff --git a/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch b/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch
new file mode 100644
index 0000000000..0fdcde78a6
--- /dev/null
+++ b/patches/server/0016-Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch
@@ -0,0 +1,246 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: SirYwell <[email protected]>
+Date: Sat, 10 Jul 2021 11:12:30 +0200
+Subject: [PATCH] Rewrite LogEvents to contain the source jars in stack traces
+
+
+diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java b/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6ffd1befe64c6c3036c22e05ed1c44808d64bd28
+--- /dev/null
++++ b/src/log4jPlugins/java/io/papermc/paper/logging/DelegateLogEvent.java
+@@ -0,0 +1,130 @@
++package io.papermc.paper.logging;
++
++import org.apache.logging.log4j.Level;
++import org.apache.logging.log4j.Marker;
++import org.apache.logging.log4j.ThreadContext;
++import org.apache.logging.log4j.core.LogEvent;
++import org.apache.logging.log4j.core.impl.ThrowableProxy;
++import org.apache.logging.log4j.core.time.Instant;
++import org.apache.logging.log4j.message.Message;
++import org.apache.logging.log4j.util.ReadOnlyStringMap;
++
++import java.util.Map;
++
++public class DelegateLogEvent implements LogEvent {
++ private final LogEvent original;
++
++ protected DelegateLogEvent(LogEvent original) {
++ this.original = original;
++ }
++
++ @Override
++ public LogEvent toImmutable() {
++ return this.original.toImmutable();
++ }
++
++ @Override
++ public Map<String, String> getContextMap() {
++ return this.original.getContextMap();
++ }
++
++ @Override
++ public ReadOnlyStringMap getContextData() {
++ return this.original.getContextData();
++ }
++
++ @Override
++ public ThreadContext.ContextStack getContextStack() {
++ return this.original.getContextStack();
++ }
++
++ @Override
++ public String getLoggerFqcn() {
++ return this.original.getLoggerFqcn();
++ }
++
++ @Override
++ public Level getLevel() {
++ return this.original.getLevel();
++ }
++
++ @Override
++ public String getLoggerName() {
++ return this.original.getLoggerName();
++ }
++
++ @Override
++ public Marker getMarker() {
++ return this.original.getMarker();
++ }
++
++ @Override
++ public Message getMessage() {
++ return this.original.getMessage();
++ }
++
++ @Override
++ public long getTimeMillis() {
++ return this.original.getTimeMillis();
++ }
++
++ @Override
++ public Instant getInstant() {
++ return this.original.getInstant();
++ }
++
++ @Override
++ public StackTraceElement getSource() {
++ return this.original.getSource();
++ }
++
++ @Override
++ public String getThreadName() {
++ return this.original.getThreadName();
++ }
++
++ @Override
++ public long getThreadId() {
++ return this.original.getThreadId();
++ }
++
++ @Override
++ public int getThreadPriority() {
++ return this.original.getThreadPriority();
++ }
++
++ @Override
++ public Throwable getThrown() {
++ return this.original.getThrown();
++ }
++
++ @Override
++ public ThrowableProxy getThrownProxy() {
++ return this.original.getThrownProxy();
++ }
++
++ @Override
++ public boolean isEndOfBatch() {
++ return this.original.isEndOfBatch();
++ }
++
++ @Override
++ public boolean isIncludeLocation() {
++ return this.original.isIncludeLocation();
++ }
++
++ @Override
++ public void setEndOfBatch(boolean endOfBatch) {
++ this.original.setEndOfBatch(endOfBatch);
++ }
++
++ @Override
++ public void setIncludeLocation(boolean locationRequired) {
++ this.original.setIncludeLocation(locationRequired);
++ }
++
++ @Override
++ public long getNanoTime() {
++ return this.original.getNanoTime();
++ }
++}
+diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..558427c65b4051923f73d15d85ee519be005060a
+--- /dev/null
++++ b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoLogEvent.java
+@@ -0,0 +1,48 @@
++package io.papermc.paper.logging;
++
++import org.apache.logging.log4j.core.LogEvent;
++import org.apache.logging.log4j.core.impl.ExtendedClassInfo;
++import org.apache.logging.log4j.core.impl.ExtendedStackTraceElement;
++import org.apache.logging.log4j.core.impl.ThrowableProxy;
++
++public class ExtraClassInfoLogEvent extends DelegateLogEvent {
++
++ private boolean fixed;
++
++ public ExtraClassInfoLogEvent(LogEvent original) {
++ super(original);
++ }
++
++ @Override
++ public ThrowableProxy getThrownProxy() {
++ if (fixed) {
++ return super.getThrownProxy();
++ }
++ rewriteStackTrace(super.getThrownProxy());
++ fixed = true;
++ return super.getThrownProxy();
++ }
++
++ private void rewriteStackTrace(ThrowableProxy throwable) {
++ ExtendedStackTraceElement[] stackTrace = throwable.getExtendedStackTrace();
++ for (int i = 0; i < stackTrace.length; i++) {
++ ExtendedClassInfo classInfo = stackTrace[i].getExtraClassInfo();
++ if (classInfo.getLocation().equals("?")) {
++ StackTraceElement element = stackTrace[i].getStackTraceElement();
++ String classLoaderName = element.getClassLoaderName();
++ if (classLoaderName != null) {
++ stackTrace[i] = new ExtendedStackTraceElement(element,
++ new ExtendedClassInfo(classInfo.getExact(), classLoaderName, "?"));
++ }
++ }
++ }
++ if (throwable.getCauseProxy() != null) {
++ rewriteStackTrace(throwable.getCauseProxy());
++ }
++ if (throwable.getSuppressedProxies() != null) {
++ for (ThrowableProxy proxy : throwable.getSuppressedProxies()) {
++ rewriteStackTrace(proxy);
++ }
++ }
++ }
++}
+diff --git a/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..34734bb969a1a74c7a4f9c17d40ebf007ad5d701
+--- /dev/null
++++ b/src/log4jPlugins/java/io/papermc/paper/logging/ExtraClassInfoRewritePolicy.java
+@@ -0,0 +1,29 @@
++package io.papermc.paper.logging;
++
++import org.apache.logging.log4j.core.Core;
++import org.apache.logging.log4j.core.LogEvent;
++import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
++import org.apache.logging.log4j.core.config.plugins.Plugin;
++import org.apache.logging.log4j.core.config.plugins.PluginFactory;
++import org.jetbrains.annotations.NotNull;
++
++@Plugin(
++ name = "ExtraClassInfoRewritePolicy",
++ category = Core.CATEGORY_NAME,
++ elementType = "rewritePolicy",
++ printObject = true
++)
++public final class ExtraClassInfoRewritePolicy implements RewritePolicy {
++ @Override
++ public LogEvent rewrite(LogEvent source) {
++ if (source.getThrown() != null) {
++ return new ExtraClassInfoLogEvent(source);
++ }
++ return source;
++ }
++
++ @PluginFactory
++ public static @NotNull ExtraClassInfoRewritePolicy createPolicy() {
++ return new ExtraClassInfoRewritePolicy();
++ }
++}
+diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
+index 128fa1376f22d3429a23d79a2772abf2e7fec2bc..637d64da9938e51a97338b9253b43889585c67bb 100644
+--- a/src/main/resources/log4j2.xml
++++ b/src/main/resources/log4j2.xml
+@@ -34,6 +34,10 @@
+ </Async>
+ <Rewrite name="rewrite">
+ <StacktraceDeobfuscatingRewritePolicy />
++ <AppenderRef ref="rewrite2"/>
++ </Rewrite>
++ <Rewrite name="rewrite2">
++ <ExtraClassInfoRewritePolicy />
+ <AppenderRef ref="File"/>
+ <AppenderRef ref="TerminalConsole" level="info"/>
+ <AppenderRef ref="ServerGuiConsole" level="info"/>
diff --git a/patches/server/0017-Paper-command.patch b/patches/server/0017-Paper-command.patch
new file mode 100644
index 0000000000..58246e59fd
--- /dev/null
+++ b/patches/server/0017-Paper-command.patch
@@ -0,0 +1,665 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Mon, 29 Feb 2016 21:02:09 -0600
+Subject: [PATCH] Paper command
+
+Co-authored-by: Zach Brown <[email protected]>
+
+diff --git a/src/main/java/io/papermc/paper/command/CallbackCommand.java b/src/main/java/io/papermc/paper/command/CallbackCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fa4202abd13c1c286bd398938103d1103d5443e7
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/CallbackCommand.java
+@@ -0,0 +1,35 @@
++package io.papermc.paper.command;
++
++import io.papermc.paper.adventure.providers.ClickCallbackProviderImpl;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import java.util.UUID;
++
++@DefaultQualifier(NonNull.class)
++public class CallbackCommand extends Command {
++
++ protected CallbackCommand(final String name) {
++ super(name);
++ this.description = "ClickEvent callback";
++ this.usageMessage = "/callback <uuid>";
++ }
++
++ @Override
++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) {
++ if (args.length != 1) {
++ return false;
++ }
++
++ final UUID id;
++ try {
++ id = UUID.fromString(args[0]);
++ } catch (final IllegalArgumentException ignored) {
++ return false;
++ }
++
++ ClickCallbackProviderImpl.CALLBACK_MANAGER.runCallback(sender, id);
++ return true;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/CommandUtil.java b/src/main/java/io/papermc/paper/command/CommandUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..953c30500892e5f0c55b8597bc708ea85bf56d6e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/CommandUtil.java
+@@ -0,0 +1,69 @@
++package io.papermc.paper.command;
++
++import com.google.common.base.Functions;
++import com.google.common.collect.Iterables;
++import com.google.common.collect.Lists;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
++import java.util.Iterator;
++import java.util.List;
++import net.minecraft.resources.ResourceLocation;
++import org.bukkit.command.CommandSender;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class CommandUtil {
++ private CommandUtil() {
++ }
++
++ // Code from Mojang - copyright them
++ public static List<String> getListMatchingLast(
++ final CommandSender sender,
++ final String[] args,
++ final String... matches
++ ) {
++ return getListMatchingLast(sender, args, Arrays.asList(matches));
++ }
++
++ public static boolean matches(final String s, final String s1) {
++ return s1.regionMatches(true, 0, s, 0, s.length());
++ }
++
++ public static List<String> getListMatchingLast(
++ final CommandSender sender,
++ final String[] strings,
++ final Collection<?> collection
++ ) {
++ String last = strings[strings.length - 1];
++ ArrayList<String> results = Lists.newArrayList();
++
++ if (!collection.isEmpty()) {
++ Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator();
++
++ while (iterator.hasNext()) {
++ String s1 = (String) iterator.next();
++
++ if (matches(last, s1) && (sender.hasPermission(PaperCommand.BASE_PERM + s1) || sender.hasPermission("bukkit.command.paper"))) {
++ results.add(s1);
++ }
++ }
++
++ if (results.isEmpty()) {
++ iterator = collection.iterator();
++
++ while (iterator.hasNext()) {
++ Object object = iterator.next();
++
++ if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) {
++ results.add(String.valueOf(object));
++ }
++ }
++ }
++ }
++
++ return results;
++ }
++ // end copy stuff
++}
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..95d3320c865d24609252063be07cddfd07d0d6d8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
+@@ -0,0 +1,143 @@
++package io.papermc.paper.command;
++
++import io.papermc.paper.command.subcommands.*;
++import it.unimi.dsi.fastutil.Pair;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collections;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Set;
++import java.util.stream.Collectors;
++import net.minecraft.Util;
++import org.bukkit.Bukkit;
++import org.bukkit.Location;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginManager;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++
++@DefaultQualifier(NonNull.class)
++public final class PaperCommand extends Command {
++ static final String BASE_PERM = "bukkit.command.paper.";
++ // subcommand label -> subcommand
++ private static final Map<String, PaperSubcommand> SUBCOMMANDS = Util.make(() -> {
++ final Map<Set<String>, PaperSubcommand> commands = new HashMap<>();
++
++ commands.put(Set.of("heap"), new HeapDumpCommand());
++ commands.put(Set.of("entity"), new EntityCommand());
++ commands.put(Set.of("reload"), new ReloadCommand());
++ commands.put(Set.of("version"), new VersionCommand());
++
++ return commands.entrySet().stream()
++ .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
++ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
++ });
++ private static final Set<String> COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet());
++ // alias -> subcommand label
++ private static final Map<String, String> ALIASES = Util.make(() -> {
++ final Map<String, Set<String>> aliases = new HashMap<>();
++
++ aliases.put("version", Set.of("ver"));
++
++ return aliases.entrySet().stream()
++ .flatMap(entry -> entry.getValue().stream().map(s -> Map.entry(s, entry.getKey())))
++ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
++ });
++
++ public PaperCommand(final String name) {
++ super(name);
++ this.description = "Paper related commands";
++ this.usageMessage = "/paper [" + String.join(" | ", SUBCOMMANDS.keySet()) + "]";
++ final List<String> permissions = new ArrayList<>();
++ permissions.add("bukkit.command.paper");
++ permissions.addAll(SUBCOMMANDS.keySet().stream().map(s -> BASE_PERM + s).toList());
++ this.setPermission(String.join(";", permissions));
++ final PluginManager pluginManager = Bukkit.getServer().getPluginManager();
++ for (final String perm : permissions) {
++ pluginManager.addPermission(new Permission(perm, PermissionDefault.OP));
++ }
++ }
++
++ private static boolean testPermission(final CommandSender sender, final String permission) {
++ if (sender.hasPermission(BASE_PERM + permission) || sender.hasPermission("bukkit.command.paper")) {
++ return true;
++ }
++ sender.sendMessage(text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", RED));
++ return false;
++ }
++
++ @Override
++ public List<String> tabComplete(
++ final CommandSender sender,
++ final String alias,
++ final String[] args,
++ final @Nullable Location location
++ ) throws IllegalArgumentException {
++ if (args.length <= 1) {
++ return CommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS);
++ }
++
++ final @Nullable Pair<String, PaperSubcommand> subCommand = resolveCommand(args[0]);
++ if (subCommand != null) {
++ return subCommand.second().tabComplete(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length));
++ }
++
++ return Collections.emptyList();
++ }
++
++ @Override
++ public boolean execute(
++ final CommandSender sender,
++ final String commandLabel,
++ final String[] args
++ ) {
++ if (!testPermission(sender)) {
++ return true;
++ }
++
++ if (args.length == 0) {
++ sender.sendMessage(text("Usage: " + this.usageMessage, RED));
++ return false;
++ }
++ final @Nullable Pair<String, PaperSubcommand> subCommand = resolveCommand(args[0]);
++
++ if (subCommand == null) {
++ sender.sendMessage(text("Usage: " + this.usageMessage, RED));
++ return false;
++ }
++
++ if (!testPermission(sender, subCommand.first())) {
++ return true;
++ }
++ final String[] choppedArgs = Arrays.copyOfRange(args, 1, args.length);
++ return subCommand.second().execute(sender, subCommand.first(), choppedArgs);
++ }
++
++ private static @Nullable Pair<String, PaperSubcommand> resolveCommand(String label) {
++ label = label.toLowerCase(Locale.ROOT);
++ @Nullable PaperSubcommand subCommand = SUBCOMMANDS.get(label);
++ if (subCommand == null) {
++ final @Nullable String command = ALIASES.get(label);
++ if (command != null) {
++ label = command;
++ subCommand = SUBCOMMANDS.get(command);
++ }
++ }
++
++ if (subCommand != null) {
++ return Pair.of(label, subCommand);
++ }
++
++ return null;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5e5ec700a368cfdaa1ea0b3f0fa82089895d4b92
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
+@@ -0,0 +1,28 @@
++package io.papermc.paper.command;
++
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.command.Command;
++
++import java.util.HashMap;
++import java.util.Map;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class PaperCommands {
++
++ private PaperCommands() {
++ }
++
++ private static final Map<String, Command> COMMANDS = new HashMap<>();
++ static {
++ COMMANDS.put("paper", new PaperCommand("paper"));
++ COMMANDS.put("callback", new CallbackCommand("callback"));
++ }
++
++ public static void registerCommands(final MinecraftServer server) {
++ COMMANDS.forEach((s, command) -> {
++ server.server.getCommandMap().register(s, "Paper", command);
++ });
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/PaperSubcommand.java b/src/main/java/io/papermc/paper/command/PaperSubcommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7e9e0ff8639be135bf8575e375cbada5b57164e1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/PaperSubcommand.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.command;
++
++import java.util.Collections;
++import java.util.List;
++import org.bukkit.command.CommandSender;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperSubcommand {
++ boolean execute(CommandSender sender, String subCommand, String[] args);
++
++ default List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
++ return Collections.emptyList();
++ }
++
++ default boolean tabCompletes() {
++ return true;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9d9d133e0d973ecda1ef1efc872a51ee10463fd1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java
+@@ -0,0 +1,158 @@
++package io.papermc.paper.command.subcommands;
++
++import com.google.common.collect.Maps;
++import io.papermc.paper.command.CommandUtil;
++import io.papermc.paper.command.PaperSubcommand;
++import java.util.Collections;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Set;
++import java.util.stream.Collectors;
++
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.event.HoverEvent;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.level.ServerChunkCache;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.level.ChunkPos;
++import org.apache.commons.lang3.tuple.MutablePair;
++import org.apache.commons.lang3.tuple.Pair;
++import org.bukkit.Bukkit;
++import org.bukkit.HeightMap;
++import org.bukkit.World;
++import org.bukkit.command.CommandSender;
++import org.bukkit.craftbukkit.CraftWorld;
++import org.bukkit.entity.Player;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++
++@DefaultQualifier(NonNull.class)
++public final class EntityCommand implements PaperSubcommand {
++ @Override
++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++ this.listEntities(sender, args);
++ return true;
++ }
++
++ @Override
++ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
++ if (args.length == 1) {
++ return CommandUtil.getListMatchingLast(sender, args, "help", "list");
++ } else if (args.length == 2) {
++ return CommandUtil.getListMatchingLast(sender, args, BuiltInRegistries.ENTITY_TYPE.keySet().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new));
++ }
++ return Collections.emptyList();
++ }
++
++ /*
++ * Ported from MinecraftForge - author: LexManos <[email protected]> - License: LGPLv2.1
++ */
++ private void listEntities(final CommandSender sender, final String[] args) {
++ // help
++ if (args.length < 1 || !args[0].toLowerCase(Locale.ROOT).equals("list")) {
++ sender.sendMessage(text("Use /paper entity [list] help for more information on a specific command", RED));
++ return;
++ }
++
++ if ("list".equals(args[0].toLowerCase(Locale.ROOT))) {
++ String filter = "*";
++ if (args.length > 1) {
++ if (args[1].toLowerCase(Locale.ROOT).equals("help")) {
++ sender.sendMessage(text("Use /paper entity list [filter] [worldName] to get entity info that matches the optional filter.", RED));
++ return;
++ }
++ filter = args[1];
++ }
++ final String cleanfilter = filter.replace("?", ".?").replace("*", ".*?");
++ Set<ResourceLocation> names = BuiltInRegistries.ENTITY_TYPE.keySet().stream()
++ .filter(n -> n.toString().matches(cleanfilter))
++ .collect(Collectors.toSet());
++ if (names.isEmpty()) {
++ sender.sendMessage(text("Invalid filter, does not match any entities. Use /paper entity list for a proper list", RED));
++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED));
++ return;
++ }
++ String worldName;
++ if (args.length > 2) {
++ worldName = args[2];
++ } else if (sender instanceof Player) {
++ worldName = ((Player) sender).getWorld().getName();
++ } else {
++ sender.sendMessage(text("Please specify the name of a world", RED));
++ sender.sendMessage(text("To do so without a filter, specify '*' as the filter", RED));
++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED));
++ return;
++ }
++ Map<ResourceLocation, MutablePair<Integer, Map<ChunkPos, Integer>>> list = Maps.newHashMap();
++ @Nullable World bukkitWorld = Bukkit.getWorld(worldName);
++ if (bukkitWorld == null) {
++ sender.sendMessage(text("Could not load world for " + worldName + ". Please select a valid world.", RED));
++ sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED));
++ return;
++ }
++ ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
++ Map<ResourceLocation, Integer> nonEntityTicking = Maps.newHashMap();
++ ServerChunkCache chunkProviderServer = world.getChunkSource();
++ world.getAllEntities().forEach(e -> {
++ ResourceLocation key = EntityType.getKey(e.getType());
++
++ MutablePair<Integer, Map<ChunkPos, Integer>> info = list.computeIfAbsent(key, k -> MutablePair.of(0, Maps.newHashMap()));
++ ChunkPos chunk = e.chunkPosition();
++ info.left++;
++ info.right.put(chunk, info.right.getOrDefault(chunk, 0) + 1);
++ if (!world.isPositionEntityTicking(e.blockPosition())) {
++ nonEntityTicking.merge(key, 1, Integer::sum);
++ }
++ });
++ if (names.size() == 1) {
++ ResourceLocation name = names.iterator().next();
++ Pair<Integer, Map<ChunkPos, Integer>> info = list.get(name);
++ int nonTicking = nonEntityTicking.getOrDefault(name, 0);
++ if (info == null) {
++ sender.sendMessage(text("No entities found.", RED));
++ return;
++ }
++ sender.sendMessage("Entity: " + name + " Total Ticking: " + (info.getLeft() - nonTicking) + ", Total Non-Ticking: " + nonTicking);
++ info.getRight().entrySet().stream()
++ .sorted((a, b) -> !a.getValue().equals(b.getValue()) ? b.getValue() - a.getValue() : a.getKey().toString().compareTo(b.getKey().toString()))
++ .limit(10).forEach(e -> {
++ final int x = (e.getKey().x << 4) + 8;
++ final int z = (e.getKey().z << 4) + 8;
++ final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)"))
++ .hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN)))
++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z));
++ sender.sendMessage(message);
++ });
++ } else {
++ List<Pair<ResourceLocation, Integer>> info = list.entrySet().stream()
++ .filter(e -> names.contains(e.getKey()))
++ .map(e -> Pair.of(e.getKey(), e.getValue().left))
++ .sorted((a, b) -> !a.getRight().equals(b.getRight()) ? b.getRight() - a.getRight() : a.getKey().toString().compareTo(b.getKey().toString()))
++ .toList();
++
++ if (info.isEmpty()) {
++ sender.sendMessage(text("No entities found.", RED));
++ return;
++ }
++
++ int count = info.stream().mapToInt(Pair::getRight).sum();
++ int nonTickingCount = nonEntityTicking.values().stream().mapToInt(Integer::intValue).sum();
++ sender.sendMessage("Total Ticking: " + (count - nonTickingCount) + ", Total Non-Ticking: " + nonTickingCount);
++ info.forEach(e -> {
++ int nonTicking = nonEntityTicking.getOrDefault(e.getKey(), 0);
++ sender.sendMessage(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey());
++ });
++ sender.sendMessage("* First number is ticking entities, second number is non-ticking entities");
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cd2e4d792e972b8bf1e07b8961594a670ae949cf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
+@@ -0,0 +1,38 @@
++package io.papermc.paper.command.subcommands;
++
++import io.papermc.paper.command.PaperSubcommand;
++import java.time.LocalDateTime;
++import java.time.format.DateTimeFormatter;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.bukkit.craftbukkit.CraftServer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++import static net.kyori.adventure.text.format.NamedTextColor.YELLOW;
++
++@DefaultQualifier(NonNull.class)
++public final class HeapDumpCommand implements PaperSubcommand {
++ @Override
++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++ this.dumpHeap(sender);
++ return true;
++ }
++
++ private void dumpHeap(final CommandSender sender) {
++ java.nio.file.Path dir = java.nio.file.Paths.get("./dumps");
++ String name = "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now());
++
++ Command.broadcastCommandMessage(sender, text("Writing JVM heap data...", YELLOW));
++
++ java.nio.file.Path file = CraftServer.dumpHeap(dir, name);
++ if (file != null) {
++ Command.broadcastCommandMessage(sender, text("Heap dump saved to " + file, GREEN));
++ } else {
++ Command.broadcastCommandMessage(sender, text("Failed to write heap dump, see server log for details", RED));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bd68139ae635f2ad7ec8e7a21e0056a139c4c62e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
+@@ -0,0 +1,33 @@
++package io.papermc.paper.command.subcommands;
++
++import io.papermc.paper.command.PaperSubcommand;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.bukkit.craftbukkit.CraftServer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++
++@DefaultQualifier(NonNull.class)
++public final class ReloadCommand implements PaperSubcommand {
++ @Override
++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++ this.doReload(sender);
++ return true;
++ }
++
++ private void doReload(final CommandSender sender) {
++ Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED));
++ Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED));
++
++ MinecraftServer server = ((CraftServer) sender.getServer()).getServer();
++ server.paperConfigurations.reloadConfigs(server);
++ server.server.reloadCount++;
++
++ Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ae60bd96b5284d54676d8e7e4dd5d170b526ec1e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.command.subcommands;
++
++import io.papermc.paper.command.PaperSubcommand;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class VersionCommand implements PaperSubcommand {
++ @Override
++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++ final @Nullable Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version");
++ if (ver != null) {
++ ver.execute(sender, "paper", new String[0]);
++ }
++ return true;
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index a61a92078a8bb4979f231c02ef5aa990b8ab57ad..cd9e4bfdb3f335213001ced27540bb7efbc04130 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -214,6 +214,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess());
+ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
+ // Paper end - initialize global and world-defaults configuration
++ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
+
+ this.setPvpAllowed(dedicatedserverproperties.pvp);
+ this.setFlightAllowed(dedicatedserverproperties.allowFlight);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 41aa22f431c989d60dde5c85ca2821d5bcf613af..118c8b227133639427c1da84b93fcaa865fd6d02 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -991,6 +991,7 @@ public final class CraftServer implements Server {
+ this.commandMap.clearCommands();
+ this.reloadData();
+ org.spigotmc.SpigotConfig.registerCommands(); // Spigot
++ io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper
+ this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*");
+ this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions");
+
+@@ -2737,6 +2738,34 @@ public final class CraftServer implements Server {
+ // Paper end
+
+ // Paper start
++ @SuppressWarnings({"rawtypes", "unchecked"})
++ public static java.nio.file.Path dumpHeap(java.nio.file.Path dir, String name) {
++ try {
++ java.nio.file.Files.createDirectories(dir);
++
++ javax.management.MBeanServer server = java.lang.management.ManagementFactory.getPlatformMBeanServer();
++ java.nio.file.Path file;
++
++ try {
++ Class clazz = Class.forName("openj9.lang.management.OpenJ9DiagnosticsMXBean");
++ Object openj9Mbean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "openj9.lang.management:type=OpenJ9Diagnostics", clazz);
++ java.lang.reflect.Method m = clazz.getMethod("triggerDumpToFile", String.class, String.class);
++ file = dir.resolve(name + ".phd");
++ m.invoke(openj9Mbean, "heap", file.toString());
++ } catch (ClassNotFoundException e) {
++ Class clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
++ Object hotspotMBean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", clazz);
++ java.lang.reflect.Method m = clazz.getMethod("dumpHeap", String.class, boolean.class);
++ file = dir.resolve(name + ".hprof");
++ m.invoke(hotspotMBean, file.toString(), true);
++ }
++
++ return file;
++ } catch (Throwable t) {
++ Bukkit.getLogger().log(Level.SEVERE, "Could not write heap", t);
++ return null;
++ }
++ }
+ private Iterable<? extends net.kyori.adventure.audience.Audience> adventure$audiences;
+ @Override
+ public Iterable<? extends net.kyori.adventure.audience.Audience> audiences() {
diff --git a/patches/server/0018-Paper-Metrics.patch b/patches/server/0018-Paper-Metrics.patch
new file mode 100644
index 0000000000..9ad0f5d70e
--- /dev/null
+++ b/patches/server/0018-Paper-Metrics.patch
@@ -0,0 +1,731 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Fri, 24 Mar 2017 23:56:01 -0500
+Subject: [PATCH] Paper Metrics
+
+Removes Spigot's mcstats metrics in favor of a system using bStats
+
+To disable for privacy or other reasons go to the bStats folder in your plugins folder
+and edit the config.yml file present there.
+
+Please keep in mind the data collected is anonymous and collection should have no
+tangible effect on server performance. The data is used to allow the authors of
+PaperMC to track version and platform usage so that we can make better management
+decisions on behalf of the project.
+
+diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6aaed8e8bf8c721fc834da5c76ac72a4c3e92458
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/Metrics.java
+@@ -0,0 +1,678 @@
++package com.destroystokyo.paper;
++
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++import org.bukkit.configuration.file.YamlConfiguration;
++import org.bukkit.craftbukkit.util.CraftMagicNumbers;
++import org.bukkit.plugin.Plugin;
++
++import org.json.simple.JSONArray;
++import org.json.simple.JSONObject;
++
++import javax.net.ssl.HttpsURLConnection;
++import java.io.ByteArrayOutputStream;
++import java.io.DataOutputStream;
++import java.io.File;
++import java.io.IOException;
++import java.net.URL;
++import java.util.*;
++import java.util.concurrent.Callable;
++import java.util.concurrent.Executors;
++import java.util.concurrent.ScheduledExecutorService;
++import java.util.concurrent.TimeUnit;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++import java.util.regex.Matcher;
++import java.util.regex.Pattern;
++import java.util.zip.GZIPOutputStream;
++
++/**
++ * bStats collects some data for plugin authors.
++ *
++ * Check out https://bStats.org/ to learn more about bStats!
++ */
++public class Metrics {
++
++ // Executor service for requests
++ // We use an executor service because the Bukkit scheduler is affected by server lags
++ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
++
++ // The version of this bStats class
++ public static final int B_STATS_VERSION = 1;
++
++ // The url to which the data is sent
++ private static final String URL = "https://bStats.org/submitData/server-implementation";
++
++ // Should failed requests be logged?
++ private static boolean logFailedRequests = false;
++
++ // The logger for the failed requests
++ private static Logger logger = Logger.getLogger("bStats");
++
++ // The name of the server software
++ private final String name;
++
++ // The uuid of the server
++ private final String serverUUID;
++
++ // A list with all custom charts
++ private final List<CustomChart> charts = new ArrayList<>();
++
++ /**
++ * Class constructor.
++ *
++ * @param name The name of the server software.
++ * @param serverUUID The uuid of the server.
++ * @param logFailedRequests Whether failed requests should be logged or not.
++ * @param logger The logger for the failed requests.
++ */
++ public Metrics(String name, String serverUUID, boolean logFailedRequests, Logger logger) {
++ this.name = name;
++ this.serverUUID = serverUUID;
++ Metrics.logFailedRequests = logFailedRequests;
++ Metrics.logger = logger;
++
++ // Start submitting the data
++ startSubmitting();
++ }
++
++ /**
++ * Adds a custom chart.
++ *
++ * @param chart The chart to add.
++ */
++ public void addCustomChart(CustomChart chart) {
++ if (chart == null) {
++ throw new IllegalArgumentException("Chart cannot be null!");
++ }
++ charts.add(chart);
++ }
++
++ /**
++ * Starts the Scheduler which submits our data every 30 minutes.
++ */
++ private void startSubmitting() {
++ final Runnable submitTask = this::submitData;
++
++ // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the
++ // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay.
++ // WARNING: You must not modify any part of this Metrics class, including the submit delay or frequency!
++ // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
++ long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
++ long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
++ scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
++ scheduler.scheduleAtFixedRate(submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
++ }
++
++ /**
++ * Gets the plugin specific data.
++ *
++ * @return The plugin specific data.
++ */
++ private JSONObject getPluginData() {
++ JSONObject data = new JSONObject();
++
++ data.put("pluginName", name); // Append the name of the server software
++ JSONArray customCharts = new JSONArray();
++ for (CustomChart customChart : charts) {
++ // Add the data of the custom charts
++ JSONObject chart = customChart.getRequestJsonObject();
++ if (chart == null) { // If the chart is null, we skip it
++ continue;
++ }
++ customCharts.add(chart);
++ }
++ data.put("customCharts", customCharts);
++
++ return data;
++ }
++
++ /**
++ * Gets the server specific data.
++ *
++ * @return The server specific data.
++ */
++ private JSONObject getServerData() {
++ // OS specific data
++ String osName = System.getProperty("os.name");
++ String osArch = System.getProperty("os.arch");
++ String osVersion = System.getProperty("os.version");
++ int coreCount = Runtime.getRuntime().availableProcessors();
++
++ JSONObject data = new JSONObject();
++
++ data.put("serverUUID", serverUUID);
++
++ data.put("osName", osName);
++ data.put("osArch", osArch);
++ data.put("osVersion", osVersion);
++ data.put("coreCount", coreCount);
++
++ return data;
++ }
++
++ /**
++ * Collects the data and sends it afterwards.
++ */
++ private void submitData() {
++ final JSONObject data = getServerData();
++
++ JSONArray pluginData = new JSONArray();
++ pluginData.add(getPluginData());
++ data.put("plugins", pluginData);
++
++ try {
++ // We are still in the Thread of the timer, so nothing get blocked :)
++ sendData(data);
++ } catch (Exception e) {
++ // Something went wrong! :(
++ if (logFailedRequests) {
++ logger.log(Level.WARNING, "Could not submit stats of " + name, e);
++ }
++ }
++ }
++
++ /**
++ * Sends the data to the bStats server.
++ *
++ * @param data The data to send.
++ * @throws Exception If the request failed.
++ */
++ private static void sendData(JSONObject data) throws Exception {
++ if (data == null) {
++ throw new IllegalArgumentException("Data cannot be null!");
++ }
++ HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
++
++ // Compress the data to save bandwidth
++ byte[] compressedData = compress(data.toString());
++
++ // Add headers
++ connection.setRequestMethod("POST");
++ connection.addRequestProperty("Accept", "application/json");
++ connection.addRequestProperty("Connection", "close");
++ connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
++ connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
++ connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
++ connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
++
++ // Send data
++ connection.setDoOutput(true);
++ DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
++ outputStream.write(compressedData);
++ outputStream.flush();
++ outputStream.close();
++
++ connection.getInputStream().close(); // We don't care about the response - Just send our data :)
++ }
++
++ /**
++ * Gzips the given String.
++ *
++ * @param str The string to gzip.
++ * @return The gzipped String.
++ * @throws IOException If the compression failed.
++ */
++ private static byte[] compress(final String str) throws IOException {
++ if (str == null) {
++ return null;
++ }
++ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
++ GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
++ gzip.write(str.getBytes("UTF-8"));
++ gzip.close();
++ return outputStream.toByteArray();
++ }
++
++ /**
++ * Represents a custom chart.
++ */
++ public static abstract class CustomChart {
++
++ // The id of the chart
++ final String chartId;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ */
++ CustomChart(String chartId) {
++ if (chartId == null || chartId.isEmpty()) {
++ throw new IllegalArgumentException("ChartId cannot be null or empty!");
++ }
++ this.chartId = chartId;
++ }
++
++ private JSONObject getRequestJsonObject() {
++ JSONObject chart = new JSONObject();
++ chart.put("chartId", chartId);
++ try {
++ JSONObject data = getChartData();
++ if (data == null) {
++ // If the data is null we don't send the chart.
++ return null;
++ }
++ chart.put("data", data);
++ } catch (Throwable t) {
++ if (logFailedRequests) {
++ logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t);
++ }
++ return null;
++ }
++ return chart;
++ }
++
++ protected abstract JSONObject getChartData() throws Exception;
++
++ }
++
++ /**
++ * Represents a custom simple pie.
++ */
++ public static class SimplePie extends CustomChart {
++
++ private final Callable<String> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public SimplePie(String chartId, Callable<String> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ String value = callable.call();
++ if (value == null || value.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("value", value);
++ return data;
++ }
++ }
++
++ /**
++ * Represents a custom advanced pie.
++ */
++ public static class AdvancedPie extends CustomChart {
++
++ private final Callable<Map<String, Integer>> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ JSONObject values = new JSONObject();
++ Map<String, Integer> map = callable.call();
++ if (map == null || map.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ boolean allSkipped = true;
++ for (Map.Entry<String, Integer> entry : map.entrySet()) {
++ if (entry.getValue() == 0) {
++ continue; // Skip this invalid
++ }
++ allSkipped = false;
++ values.put(entry.getKey(), entry.getValue());
++ }
++ if (allSkipped) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("values", values);
++ return data;
++ }
++ }
++
++ /**
++ * Represents a custom drilldown pie.
++ */
++ public static class DrilldownPie extends CustomChart {
++
++ private final Callable<Map<String, Map<String, Integer>>> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ public JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ JSONObject values = new JSONObject();
++ Map<String, Map<String, Integer>> map = callable.call();
++ if (map == null || map.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ boolean reallyAllSkipped = true;
++ for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
++ JSONObject value = new JSONObject();
++ boolean allSkipped = true;
++ for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
++ value.put(valueEntry.getKey(), valueEntry.getValue());
++ allSkipped = false;
++ }
++ if (!allSkipped) {
++ reallyAllSkipped = false;
++ values.put(entryValues.getKey(), value);
++ }
++ }
++ if (reallyAllSkipped) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("values", values);
++ return data;
++ }
++ }
++
++ /**
++ * Represents a custom single line chart.
++ */
++ public static class SingleLineChart extends CustomChart {
++
++ private final Callable<Integer> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public SingleLineChart(String chartId, Callable<Integer> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ int value = callable.call();
++ if (value == 0) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("value", value);
++ return data;
++ }
++
++ }
++
++ /**
++ * Represents a custom multi line chart.
++ */
++ public static class MultiLineChart extends CustomChart {
++
++ private final Callable<Map<String, Integer>> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ JSONObject values = new JSONObject();
++ Map<String, Integer> map = callable.call();
++ if (map == null || map.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ boolean allSkipped = true;
++ for (Map.Entry<String, Integer> entry : map.entrySet()) {
++ if (entry.getValue() == 0) {
++ continue; // Skip this invalid
++ }
++ allSkipped = false;
++ values.put(entry.getKey(), entry.getValue());
++ }
++ if (allSkipped) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("values", values);
++ return data;
++ }
++
++ }
++
++ /**
++ * Represents a custom simple bar chart.
++ */
++ public static class SimpleBarChart extends CustomChart {
++
++ private final Callable<Map<String, Integer>> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ JSONObject values = new JSONObject();
++ Map<String, Integer> map = callable.call();
++ if (map == null || map.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ for (Map.Entry<String, Integer> entry : map.entrySet()) {
++ JSONArray categoryValues = new JSONArray();
++ categoryValues.add(entry.getValue());
++ values.put(entry.getKey(), categoryValues);
++ }
++ data.put("values", values);
++ return data;
++ }
++
++ }
++
++ /**
++ * Represents a custom advanced bar chart.
++ */
++ public static class AdvancedBarChart extends CustomChart {
++
++ private final Callable<Map<String, int[]>> callable;
++
++ /**
++ * Class constructor.
++ *
++ * @param chartId The id of the chart.
++ * @param callable The callable which is used to request the chart data.
++ */
++ public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
++ super(chartId);
++ this.callable = callable;
++ }
++
++ @Override
++ protected JSONObject getChartData() throws Exception {
++ JSONObject data = new JSONObject();
++ JSONObject values = new JSONObject();
++ Map<String, int[]> map = callable.call();
++ if (map == null || map.isEmpty()) {
++ // Null = skip the chart
++ return null;
++ }
++ boolean allSkipped = true;
++ for (Map.Entry<String, int[]> entry : map.entrySet()) {
++ if (entry.getValue().length == 0) {
++ continue; // Skip this invalid
++ }
++ allSkipped = false;
++ JSONArray categoryValues = new JSONArray();
++ for (int categoryValue : entry.getValue()) {
++ categoryValues.add(categoryValue);
++ }
++ values.put(entry.getKey(), categoryValues);
++ }
++ if (allSkipped) {
++ // Null = skip the chart
++ return null;
++ }
++ data.put("values", values);
++ return data;
++ }
++
++ }
++
++ public static class PaperMetrics {
++ public static void startMetrics() {
++ // Get the config file
++ File configFile = new File(new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "bStats"), "config.yml");
++ YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
++
++ // Check if the config file exists
++ if (!config.isSet("serverUuid")) {
++
++ // Add default values
++ config.addDefault("enabled", true);
++ // Every server gets it's unique random id.
++ config.addDefault("serverUuid", UUID.randomUUID().toString());
++ // Should failed request be logged?
++ config.addDefault("logFailedRequests", false);
++
++ // Inform the server owners about bStats
++ config.options().header(
++ "bStats collects some data for plugin authors like how many servers are using their plugins.\n" +
++ "To honor their work, you should not disable it.\n" +
++ "This has nearly no effect on the server performance!\n" +
++ "Check out https://bStats.org/ to learn more :)"
++ ).copyDefaults(true);
++ try {
++ config.save(configFile);
++ } catch (IOException ignored) {
++ }
++ }
++ // Load the data
++ String serverUUID = config.getString("serverUuid");
++ boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
++ // Only start Metrics, if it's enabled in the config
++ if (config.getBoolean("enabled", true)) {
++ Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
++
++ metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
++ String minecraftVersion = Bukkit.getVersion();
++ minecraftVersion = minecraftVersion.substring(minecraftVersion.indexOf("MC: ") + 4, minecraftVersion.length() - 1);
++ return minecraftVersion;
++ }));
++
++ metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size()));
++ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline"));
++ final String paperVersion;
++ final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion();
++ if (implVersion != null) {
++ final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1);
++ paperVersion = "git-Paper-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash);
++ } else {
++ paperVersion = "unknown";
++ }
++ metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion));
++
++ metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
++ Map<String, Map<String, Integer>> map = new HashMap<>();
++ String javaVersion = System.getProperty("java.version");
++ Map<String, Integer> entry = new HashMap<>();
++ entry.put(javaVersion, 1);
++
++ // http://openjdk.java.net/jeps/223
++ // Java decided to change their versioning scheme and in doing so modified the java.version system
++ // property to return $major[.$minor][.$secuity][-ea], as opposed to 1.$major.0_$identifier
++ // we can handle pre-9 by checking if the "major" is equal to "1", otherwise, 9+
++ String majorVersion = javaVersion.split("\\.")[0];
++ String release;
++
++ int indexOf = javaVersion.lastIndexOf('.');
++
++ if (majorVersion.equals("1")) {
++ release = "Java " + javaVersion.substring(0, indexOf);
++ } else {
++ // of course, it really wouldn't be all that simple if they didn't add a quirk, now would it
++ // valid strings for the major may potentially include values such as -ea to deannotate a pre release
++ Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
++ if (versionMatcher.find()) {
++ majorVersion = versionMatcher.group(0);
++ }
++ release = "Java " + majorVersion;
++ }
++ map.put(release, entry);
++
++ return map;
++ }));
++
++ metrics.addCustomChart(new Metrics.DrilldownPie("legacy_plugins", () -> {
++ Map<String, Map<String, Integer>> map = new HashMap<>();
++
++ // count legacy plugins
++ int legacy = 0;
++ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
++ if (CraftMagicNumbers.isLegacy(plugin.getDescription())) {
++ legacy++;
++ }
++ }
++
++ // insert real value as lower dimension
++ Map<String, Integer> entry = new HashMap<>();
++ entry.put(String.valueOf(legacy), 1);
++
++ // create buckets as higher dimension
++ if (legacy == 0) {
++ map.put("0 \uD83D\uDE0E", entry); // :sunglasses:
++ } else if (legacy <= 5) {
++ map.put("1-5", entry);
++ } else if (legacy <= 10) {
++ map.put("6-10", entry);
++ } else if (legacy <= 25) {
++ map.put("11-25", entry);
++ } else if (legacy <= 50) {
++ map.put("26-50", entry);
++ } else {
++ map.put("50+ \uD83D\uDE2D", entry); // :cry:
++ }
++
++ return map;
++ }));
++ }
++
++ }
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index cd9e4bfdb3f335213001ced27540bb7efbc04130..3b403e9edf4e860160dd230977870f21a0e32a7a 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -215,6 +215,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
+ // Paper end - initialize global and world-defaults configuration
+ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
++ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
+
+ this.setPvpAllowed(dedicatedserverproperties.pvp);
+ this.setFlightAllowed(dedicatedserverproperties.allowFlight);
+diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
+index 744edd40128c910c3ad2f3657bde995612e0a1e4..d9e73e37b54e2f6e13313977c76cb4212c240992 100644
+--- a/src/main/java/org/spigotmc/SpigotConfig.java
++++ b/src/main/java/org/spigotmc/SpigotConfig.java
+@@ -83,6 +83,7 @@ public class SpigotConfig
+ MinecraftServer.getServer().server.getCommandMap().register( entry.getKey(), "Spigot", entry.getValue() );
+ }
+
++ /* // Paper - Replace with our own
+ if ( SpigotConfig.metrics == null )
+ {
+ try
+@@ -94,6 +95,7 @@ public class SpigotConfig
+ Bukkit.getServer().getLogger().log( Level.SEVERE, "Could not start metrics service", ex );
+ }
+ }
++ */ // Paper end
+ }
+
+ public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public
diff --git a/patches/server/0019-Paper-Plugins.patch b/patches/server/0019-Paper-Plugins.patch
new file mode 100644
index 0000000000..0d731f6b39
--- /dev/null
+++ b/patches/server/0019-Paper-Plugins.patch
@@ -0,0 +1,8168 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <[email protected]>
+Date: Wed, 6 Jul 2022 23:00:31 -0400
+Subject: [PATCH] Paper Plugins
+
+Co-authored-by: Micah Rao <[email protected]>
+
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
+index 95d3320c865d24609252063be07cddfd07d0d6d8..5b070d158760789bbcaa984426a55d20767abe4a 100644
+--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
+@@ -37,6 +37,7 @@ public final class PaperCommand extends Command {
+ commands.put(Set.of("entity"), new EntityCommand());
+ commands.put(Set.of("reload"), new ReloadCommand());
+ commands.put(Set.of("version"), new VersionCommand());
++ commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
+
+ return commands.entrySet().stream()
+ .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
+index 5e5ec700a368cfdaa1ea0b3f0fa82089895d4b92..72f2e81b9905a0d57ed8e2a88578f62d5235c456 100644
+--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
+@@ -24,5 +24,6 @@ public final class PaperCommands {
+ COMMANDS.forEach((s, command) -> {
+ server.server.getCommandMap().register(s, "Paper", command);
+ });
++ server.server.getCommandMap().register("bukkit", new PaperPluginsCommand());
+ }
+ }
+diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f0fce4113fb07c64adbec029d177c236cbdcbae8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
+@@ -0,0 +1,215 @@
++package io.papermc.paper.command;
++
++import com.google.common.collect.Lists;
++import io.leangen.geantyref.GenericTypeReflector;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.JoinConfiguration;
++import net.kyori.adventure.text.TextComponent;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.TextColor;
++import org.bukkit.Bukkit;
++import org.bukkit.command.CommandSender;
++import org.bukkit.command.defaults.BukkitCommand;
++import org.bukkit.craftbukkit.util.CraftMagicNumbers;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.lang.reflect.Type;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collections;
++import java.util.List;
++import java.util.TreeMap;
++
++public class PaperPluginsCommand extends BukkitCommand {
++
++ private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218);
++
++ // TODO: LINK?
++ private static final Component SERVER_PLUGIN_INFO = Component.text("ℹ What is a server plugin?", INFO_COLOR)
++ .append(asPlainComponents("""
++ Server plugins can add new behavior to your server!
++ You can find new plugins on Paper's plugin repository, Hangar.
++
++ <link to hangar>
++ """));
++
++ private static final Component SERVER_INITIALIZER_INFO = Component.text("ℹ What is a server initializer?", INFO_COLOR)
++ .append(asPlainComponents("""
++ Server initializers are ran before your server
++ starts and are provided by paper plugins.
++ """));
++
++ private static final Component LEGACY_PLUGIN_INFO = Component.text("ℹ What is a legacy plugin?", INFO_COLOR)
++ .append(asPlainComponents("""
++ A legacy plugin is a plugin that was made on
++ very old unsupported versions of the game.
++
++ It is encouraged that you replace this plugin,
++ as they might not work in the future and may cause
++ performance issues.
++ """));
++
++ private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO);
++ private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR);
++ private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209));
++ private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6));
++ private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY);
++ private static final Component PLUGIN_TICK_EMPTY = Component.text(" ");
++
++ private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken<PluginProvider<JavaPlugin>>() {}.getType();
++
++ public PaperPluginsCommand() {
++ super("plugins");
++ this.description = "Gets a list of plugins running on the server";
++ this.usageMessage = "/plugins";
++ this.setPermission("bukkit.command.plugins");
++ this.setAliases(Arrays.asList("pl"));
++ }
++
++ private static <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) {
++ List<Component> components = new ArrayList<>(plugins.size());
++ for (PluginProvider<T> entry : plugins.values()) {
++ components.add(formatProvider(entry));
++ }
++
++ boolean isFirst = true;
++ List<Component> formattedSublists = new ArrayList<>();
++ /*
++ Split up the plugin list for each 10 plugins to get size down
++
++ Plugin List:
++ - Plugin 1, Plugin 2, .... Plugin 10,
++ Plugin 11, Plugin 12 ... Plugin 20,
++ */
++ for (List<Component> componentSublist : Lists.partition(components, 10)) {
++ Component component = Component.space();
++ if (isFirst) {
++ component = component.append(PLUGIN_TICK);
++ isFirst = false;
++ } else {
++ component = PLUGIN_TICK_EMPTY;
++ //formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring.
++ }
++
++ formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist)));
++ }
++
++ return formattedSublists;
++ }
++
++ private static Component formatProvider(PluginProvider<?> provider) {
++ TextComponent.Builder builder = Component.text();
++ if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) {
++ builder.append(LEGACY_PLUGIN_STAR);
++ }
++
++ String name = provider.getMeta().getName();
++ Component pluginName = Component.text(name, fromStatus(provider))
++ .clickEvent(ClickEvent.runCommand("/version " + name));
++
++ builder.append(pluginName);
++
++ return builder.build();
++ }
++
++ private static Component asPlainComponents(String strings) {
++ net.kyori.adventure.text.TextComponent.Builder builder = Component.text();
++ for (String string : strings.split("\n")) {
++ builder.append(Component.newline());
++ builder.append(Component.text(string, NamedTextColor.WHITE));
++ }
++
++ return builder.build();
++ }
++
++ private static TextColor fromStatus(PluginProvider<?> provider) {
++ if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) {
++ ProviderStatus status = statusHolder.getLastProvidedStatus();
++
++ // Handle enabled/disabled game plugins
++ if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) {
++ Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName());
++ // Plugin doesn't exist? Could be due to it being removed.
++ if (plugin == null) {
++ return NamedTextColor.RED;
++ }
++
++ return plugin.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
++ }
++
++ return switch (status) {
++ case INITIALIZED -> NamedTextColor.GREEN;
++ case ERRORED -> NamedTextColor.RED;
++ };
++ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) {
++ // Paper plugins will be skipped if their provider is skipped due to their initializer failing.
++ // Show them as red
++ return NamedTextColor.RED;
++ } else {
++ // Separated for future logic choice, but this indicated a provider that failed to load due to
++ // dependency issues or what not.
++ return NamedTextColor.RED;
++ }
++ }
++
++ @Override
++ public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
++ if (!this.testPermission(sender)) return true;
++
++ TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
++ TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
++
++
++ for (PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) {
++ PluginMeta configuration = provider.getMeta();
++
++ if (provider instanceof SpigotPluginProvider) {
++ spigotPlugins.put(configuration.getDisplayName(), provider);
++ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++ paperPlugins.put(configuration.getDisplayName(), provider);
++ }
++ }
++
++ Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE);
++ //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs
++
++ sender.sendMessage(infoMessage);
++
++ if (!paperPlugins.isEmpty()) {
++ sender.sendMessage(PAPER_HEADER);
++ }
++
++ for (Component component : formatProviders(paperPlugins)) {
++ sender.sendMessage(component);
++ }
++
++ if (!spigotPlugins.isEmpty()) {
++ sender.sendMessage(BUKKIT_HEADER);
++ }
++
++ for (Component component : formatProviders(spigotPlugins)) {
++ sender.sendMessage(component);
++ }
++
++ return true;
++ }
++
++ @NotNull
++ @Override
++ public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
++ return Collections.emptyList();
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d4a092243e587e3a555fbc0f00c8f78c00b3d1c6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
+@@ -0,0 +1,203 @@
++package io.papermc.paper.command.subcommands;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.gson.JsonArray;
++import com.google.gson.JsonElement;
++import com.google.gson.JsonObject;
++import com.google.gson.JsonPrimitive;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import io.papermc.paper.command.PaperSubcommand;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
++import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.classloader.group.SpigotPluginClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.classloader.group.StaticPluginClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import io.papermc.paper.plugin.storage.ConfiguredProviderStorage;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.command.CommandSender;
++import org.bukkit.plugin.Plugin;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import java.io.PrintStream;
++import java.io.StringWriter;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.time.LocalDateTime;
++import java.time.format.DateTimeFormatter;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++
++@DefaultQualifier(NonNull.class)
++public final class DumpPluginsCommand implements PaperSubcommand {
++ @Override
++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++ this.dumpPlugins(sender, args);
++ return true;
++ }
++
++ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
++
++ private void dumpPlugins(final CommandSender sender, final String[] args) {
++ Path parent = Path.of("debug");
++ Path path = parent.resolve("plugin-info-" + FORMATTER.format(LocalDateTime.now()) + ".txt");
++ try {
++ Files.createDirectories(parent);
++ Files.createFile(path);
++ sender.sendMessage(text("Writing plugin information to " + path, GREEN));
++
++ final JsonObject data = this.writeDebug();
++
++ StringWriter stringWriter = new StringWriter();
++ JsonWriter jsonWriter = new JsonWriter(stringWriter);
++ jsonWriter.setIndent(" ");
++ jsonWriter.setLenient(false);
++ Streams.write(data, jsonWriter);
++
++ try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) {
++ out.print(stringWriter);
++ }
++ sender.sendMessage(text("Successfully written plugin debug information!", GREEN));
++ } catch (Throwable e) {
++ sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED));
++ MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e);
++ }
++ }
++
++ private JsonObject writeDebug() {
++ JsonObject root = new JsonObject();
++ if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) {
++ root.addProperty("legacy-loading-strategy", true);
++ }
++
++ this.writeProviders(root);
++ this.writePlugins(root);
++ this.writeClassloaders(root);
++
++ return root;
++ }
++
++ private void writeProviders(JsonObject root) {
++ JsonObject rootProviders = new JsonObject();
++ root.add("providers", rootProviders);
++
++ for (Map.Entry<Entrypoint<?>, ProviderStorage<?>> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) {
++ JsonObject entrypoint = new JsonObject();
++
++ JsonArray providers = new JsonArray();
++ entrypoint.add("providers", providers);
++
++ List<PluginProvider<Object>> pluginProviders = new ArrayList<>();
++ for (PluginProvider<?> provider : entry.getValue().getRegisteredProviders()) {
++ JsonObject providerObj = new JsonObject();
++ providerObj.addProperty("name", provider.getMeta().getName());
++ providerObj.addProperty("version", provider.getMeta().getVersion());
++ providerObj.addProperty("dependencies", provider.getMeta().getPluginDependencies().toString());
++ providerObj.addProperty("soft-dependencies", provider.getMeta().getPluginSoftDependencies().toString());
++ providerObj.addProperty("load-before", provider.getMeta().getLoadBeforePlugins().toString());
++
++
++ providers.add(providerObj);
++ pluginProviders.add((PluginProvider<Object>) provider);
++ }
++
++ JsonArray loadOrder = new JsonArray();
++ entrypoint.add("load-order", loadOrder);
++
++ ModernPluginLoadingStrategy<Object> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++ @Override
++ public void applyContext(PluginProvider<Object> provider, DependencyContext dependencyContext) {
++ }
++
++ @Override
++ public boolean load(PluginProvider<Object> provider, Object provided) {
++ return true;
++ }
++
++ @Override
++ public boolean preloadProvider(PluginProvider<Object> provider) {
++ // Don't load provider
++ loadOrder.add(provider.getMeta().getName());
++ return false;
++ }
++ });
++ modernPluginLoadingStrategy.loadProviders(pluginProviders, new SimpleMetaDependencyTree(GraphBuilder.directed().build()));
++
++ rootProviders.add(entry.getKey().getDebugName(), entrypoint);
++ }
++ }
++
++ private void writePlugins(JsonObject root) {
++ JsonArray rootPlugins = new JsonArray();
++ root.add("plugins", rootPlugins);
++
++ for (Plugin plugin : PaperPluginManagerImpl.getInstance().getPlugins()) {
++ rootPlugins.add(plugin.toString());
++ }
++ }
++
++ private void writeClassloaders(JsonObject root) {
++ JsonObject classLoadersRoot = new JsonObject();
++ root.add("classloaders", classLoadersRoot);
++
++ PaperPluginClassLoaderStorage storage = (PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance();
++ classLoadersRoot.addProperty("global", storage.getGlobalGroup().toString());
++ classLoadersRoot.addProperty("dependency_graph", PaperPluginManagerImpl.getInstance().getInstanceManagerGraph().toString());
++
++ JsonArray array = new JsonArray();
++ classLoadersRoot.add("children", array);
++ for (PluginClassLoaderGroup group : storage.getGroups()) {
++ array.add(this.writeClassloader(group));
++ }
++ }
++
++ private JsonObject writeClassloader(PluginClassLoaderGroup group) {
++ JsonObject classLoadersRoot = new JsonObject();
++ if (group instanceof SimpleListPluginClassLoaderGroup listGroup) {
++ JsonArray array = new JsonArray();
++ classLoadersRoot.addProperty("main", listGroup.toString());
++ if (group instanceof StaticPluginClassLoaderGroup staticPluginClassLoaderGroup) {
++ classLoadersRoot.addProperty("plugin-holder", staticPluginClassLoaderGroup.getPluginClassloader().toString());
++ } else if (group instanceof SpigotPluginClassLoaderGroup spigotPluginClassLoaderGroup) {
++ classLoadersRoot.addProperty("plugin-holder", spigotPluginClassLoaderGroup.getPluginClassLoader().toString());
++ }
++
++ classLoadersRoot.add("children", array);
++ for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) {
++ array.add(this.writeClassloader(innerGroup));
++ }
++
++ } else if (group instanceof LockingClassLoaderGroup locking) {
++ // Unwrap
++ return this.writeClassloader(locking.getParent());
++ } else {
++ classLoadersRoot.addProperty("raw", group.toString());
++ }
++
++ return classLoadersRoot;
++ }
++
++ private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) {
++ return new JsonPrimitive(innerGroup.toString());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..708e5bb9bbf0476fcc2c4b92c6830b094703b43e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+@@ -0,0 +1,134 @@
++package io.papermc.paper.plugin;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.configuration.PaperConfigurations;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import joptsimple.OptionSet;
++import net.minecraft.server.dedicated.DedicatedServer;
++import org.bukkit.configuration.file.YamlConfiguration;
++import org.bukkit.craftbukkit.CraftServer;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.slf4j.Logger;
++
++import java.io.File;
++import java.io.IOException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++
++public class PluginInitializerManager {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static PluginInitializerManager impl;
++ private final Path pluginDirectory;
++ private final Path updateDirectory;
++
++ PluginInitializerManager(final Path pluginDirectory, final Path updateDirectory) {
++ this.pluginDirectory = pluginDirectory;
++ this.updateDirectory = updateDirectory;
++ }
++
++ private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception {
++ // We have to load the bukkit configuration inorder to get the update folder location.
++ final File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings");
++
++ final Path pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath();
++
++ final YamlConfiguration configuration = PaperConfigurations.loadLegacyConfigFile(configFileLocationBukkit);
++
++ final String updateDirectoryName = configuration.getString("settings.update-folder", "update");
++ if (updateDirectoryName.isBlank()) {
++ return new PluginInitializerManager(pluginDirectory, null);
++ }
++
++ final Path resolvedUpdateDirectory = pluginDirectory.resolve(updateDirectoryName);
++ if (!Files.isDirectory(resolvedUpdateDirectory)) {
++ if (Files.exists(resolvedUpdateDirectory)) {
++ LOGGER.error("Misconfigured update directory!");
++ LOGGER.error("Your configured update directory ({}) in bukkit.yml is pointing to a non-directory path. " +
++ "Auto updating functionality will not work.", resolvedUpdateDirectory);
++ }
++ return new PluginInitializerManager(pluginDirectory, null);
++ }
++
++ boolean isSameFile;
++ try {
++ isSameFile = Files.isSameFile(resolvedUpdateDirectory, pluginDirectory);
++ } catch (final IOException e) {
++ LOGGER.error("Misconfigured update directory!");
++ LOGGER.error("Failed to compare update/plugin directory", e);
++ return new PluginInitializerManager(pluginDirectory, null);
++ }
++
++ if (isSameFile) {
++ LOGGER.error("Misconfigured update directory!");
++ LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to the same location as the plugin directory (%s). " +
++ "Disabling auto updating functionality.").formatted(resolvedUpdateDirectory, pluginDirectory));
++
++ return new PluginInitializerManager(pluginDirectory, null);
++ }
++
++ return new PluginInitializerManager(pluginDirectory, resolvedUpdateDirectory);
++ }
++
++ public static PluginInitializerManager init(final OptionSet optionSet) throws Exception {
++ impl = parse(optionSet);
++ return impl;
++ }
++
++ public static PluginInitializerManager instance() {
++ return impl;
++ }
++
++ @NotNull
++ public Path pluginDirectoryPath() {
++ return pluginDirectory;
++ }
++
++ @Nullable
++ public Path pluginUpdatePath() {
++ return updateDirectory;
++ }
++
++ public static void load(OptionSet optionSet) throws Exception {
++ // We have to load the bukkit configuration inorder to get the update folder location.
++ io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionSet);
++
++ // Register the default plugin directory
++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath());
++
++ // Register plugins from the flag
++ @SuppressWarnings("unchecked")
++ java.util.List<Path> files = ((java.util.List<File>) optionSet.valuesOf("add-plugin")).stream().map(File::toPath).toList();
++ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files);
++ }
++
++ // This will be the end of me...
++ public static void reload(DedicatedServer dedicatedServer) {
++ // Wipe the provider storage
++ LaunchEntryPointHandler.INSTANCE.populateProviderStorage();
++ try {
++ load(dedicatedServer.options);
++ } catch (Exception e) {
++ throw new RuntimeException("Failed to reload!", e);
++ }
++
++ boolean hasPaperPlugin = false;
++ for (PluginProvider<?> provider : LaunchEntryPointHandler.INSTANCE.getStorage().get(Entrypoint.PLUGIN).getRegisteredProviders()) {
++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++ hasPaperPlugin = true;
++ break;
++ }
++ }
++
++ if (hasPaperPlugin) {
++ LOGGER.warn("======== WARNING ========");
++ LOGGER.warn("You are reloading while having Paper plugins installed on your server.");
++ LOGGER.warn("Paper plugins do NOT support being reloaded. This will cause some unexpected issues.");
++ LOGGER.warn("=========================");
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..30b50e6294c6eaade5e17cfaf34600d122e6251c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java
+@@ -0,0 +1,48 @@
++package io.papermc.paper.plugin.bootstrap;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import java.nio.file.Path;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.jetbrains.annotations.NotNull;
++
++public final class PluginBootstrapContextImpl implements BootstrapContext {
++
++ private final PluginMeta config;
++ private final Path dataFolder;
++ private final ComponentLogger logger;
++ private final Path pluginSource;
++
++ public PluginBootstrapContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) {
++ this.config = config;
++ this.dataFolder = dataFolder;
++ this.logger = logger;
++ this.pluginSource = pluginSource;
++ }
++
++ public static PluginBootstrapContextImpl create(PluginProvider<?> provider, Path pluginFolder) {
++ Path dataFolder = pluginFolder.resolve(provider.getMeta().getName());
++
++ return new PluginBootstrapContextImpl(provider.getMeta(), dataFolder, provider.getLogger(), provider.getSource());
++ }
++
++ @Override
++ public @NotNull PluginMeta getConfiguration() {
++ return this.config;
++ }
++
++ @Override
++ public @NotNull Path getDataDirectory() {
++ return this.dataFolder;
++ }
++
++ @Override
++ public @NotNull ComponentLogger getLogger() {
++ return this.logger;
++ }
++
++ @Override
++ public @NotNull Path getPluginSource() {
++ return this.pluginSource;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ef74ca64c589c9158909a2356e73a23f4f01faa2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
+@@ -0,0 +1,49 @@
++package io.papermc.paper.plugin.bootstrap;
++
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++
++public final class PluginProviderContextImpl implements PluginProviderContext {
++
++ private final PluginMeta config;
++ private final Path dataFolder;
++ private final ComponentLogger logger;
++ private final Path pluginSource;
++
++ public PluginProviderContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) {
++ this.config = config;
++ this.dataFolder = dataFolder;
++ this.logger = logger;
++ this.pluginSource = pluginSource;
++ }
++
++ public static PluginProviderContextImpl create(PluginMeta config, ComponentLogger logger, Path pluginSource) {
++ Path dataFolder = PluginInitializerManager.instance().pluginDirectoryPath().resolve(config.getName());
++
++ return new PluginProviderContextImpl(config, dataFolder, logger, pluginSource);
++ }
++
++ @Override
++ public @NotNull PluginMeta getConfiguration() {
++ return this.config;
++ }
++
++ @Override
++ public @NotNull Path getDataDirectory() {
++ return this.dataFolder;
++ }
++
++ @Override
++ public @NotNull ComponentLogger getLogger() {
++ return this.logger;
++ }
++
++ @Override
++ public @NotNull Path getPluginSource() {
++ return this.pluginSource;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..125008ac7db8b9f3fb57c49f8e4facc4ad4bb136
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import org.bukkit.plugin.java.JavaPlugin;
++
++/**
++ * Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under.
++ * Used for loading only certain providers at a certain time.
++ * @param <T> provider type
++ */
++public final class Entrypoint<T> {
++
++ public static final Entrypoint<PluginBootstrap> BOOTSTRAPPER = new Entrypoint<>("bootstrapper");
++ public static final Entrypoint<JavaPlugin> PLUGIN = new Entrypoint<>("plugin");
++
++ private final String debugName;
++
++ private Entrypoint(String debugName) {
++ this.debugName = debugName;
++ }
++
++ public String getDebugName() {
++ return debugName;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b38e1e0f3d3055086f51bb191fd4b60ecf32d016
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
+@@ -0,0 +1,14 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++/**
++ * Represents a register that will register providers at a certain {@link Entrypoint},
++ * where then when the given {@link Entrypoint} is registered those will be loaded.
++ */
++public interface EntrypointHandler {
++
++ <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider);
++
++ void enter(Entrypoint<?> entrypoint);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..48bc745ca9632fc46b5f786ff570434702eb47f2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
+@@ -0,0 +1,74 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.storage.BootstrapProviderStorage;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
++import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.HashMap;
++import java.util.Map;
++
++/**
++ * Used by the server to register/load plugin bootstrappers and plugins.
++ */
++public class LaunchEntryPointHandler implements EntrypointHandler {
++
++ public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler();
++ private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>();
++ private final Object2BooleanMap<Entrypoint<?>> enteredMap = new Object2BooleanOpenHashMap<>();
++
++ LaunchEntryPointHandler() {
++ this.populateProviderStorage();
++ this.enteredMap.defaultReturnValue(false);
++ }
++
++ // Utility
++ public static void enterBootstrappers() {
++ LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER);
++ }
++
++ @Override
++ public void enter(Entrypoint<?> entrypoint) {
++ ProviderStorage<?> storage = this.storage.get(entrypoint);
++ if (storage == null) {
++ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
++ }
++
++ storage.enter();
++ this.enteredMap.put(entrypoint, true);
++ }
++
++ @Override
++ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
++ ProviderStorage<T> storage = this.get(entrypoint);
++ if (storage == null) {
++ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
++ }
++
++ storage.register(provider);
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T> ProviderStorage<T> get(Entrypoint<T> entrypoint) {
++ return (ProviderStorage<T>) this.storage.get(entrypoint);
++ }
++
++ // Debug only
++ @ApiStatus.Internal
++ public Map<Entrypoint<?>, ProviderStorage<?>> getStorage() {
++ return storage;
++ }
++
++ public boolean hasEntered(Entrypoint<?> entrypoint) {
++ return this.enteredMap.getBoolean(entrypoint);
++ }
++
++ // Reload only
++ public void populateProviderStorage() {
++ this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
++ this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..93b5196a960f3efbe0d28f5527ea2752426213ce
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
+@@ -0,0 +1,22 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import net.kyori.adventure.util.Services;
++import org.jetbrains.annotations.ApiStatus;
++
++public interface ClassloaderBytecodeModifier {
++
++ static ClassloaderBytecodeModifier bytecodeModifier() {
++ return Provider.INSTANCE;
++ }
++
++ byte[] modify(PluginMeta config, byte[] bytecode);
++
++ class Provider {
++
++ private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow();
++
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f9a2c55a354c877749db3f92956de802ae575788
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+@@ -0,0 +1,12 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++
++// Stub, implement in future.
++public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
++
++ @Override
++ public byte[] modify(PluginMeta configuration, byte[] bytecode) {
++ return bytecode;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..18401ede9cd1fc7094c6b74859929938e01795ca
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
+@@ -0,0 +1,209 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import org.bukkit.Bukkit;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.io.IOException;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.Enumeration;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++/**
++ * This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api.
++ * This is only used with Paper plugins.
++ *
++ * @see PaperPluginClassLoaderStorage
++ */
++public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader {
++
++ static {
++ registerAsParallelCapable();
++ }
++
++ private final URLClassLoader libraryLoader;
++ private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
++ private final Logger logger;
++ @Nullable
++ private JavaPlugin loadedJavaPlugin;
++ @Nullable
++ private PluginClassLoaderGroup group;
++
++ public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, URLClassLoader libraryLoader) throws IOException {
++ super(source, file, configuration, parentLoader);
++ this.libraryLoader = libraryLoader;
++
++ this.logger = logger;
++ if (this.configuration().hasOpenClassloader()) {
++ this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this);
++ }
++ }
++
++ private PaperPluginMeta configuration() {
++ return (PaperPluginMeta) this.configuration;
++ }
++
++ public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) {
++ if (this.configuration().hasOpenClassloader()) {
++ return;
++ }
++ if (this.group != null) {
++ // We need to unregister the classloader inorder to allow for dependencies
++ // to be recalculated
++ PaperClassLoaderStorage.instance().unregisterClassloader(this);
++ }
++
++ this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> {
++ return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration());
++ });
++ }
++
++ @Override
++ public URL getResource(String name) {
++ URL resource = findResource(name);
++ if (resource == null && this.libraryLoader != null) {
++ return this.libraryLoader.getResource(name);
++ }
++ return resource;
++ }
++
++ @Override
++ public Enumeration<URL> getResources(String name) throws IOException {
++ List<URL> resources = new ArrayList<>();
++ this.addEnumeration(resources, this.findResources(name));
++ if (this.libraryLoader != null) {
++ addEnumeration(resources, this.libraryLoader.getResources(name));
++ }
++ return Collections.enumeration(resources);
++ }
++
++ private <T> void addEnumeration(List<T> list, Enumeration<T> enumeration) {
++ while (enumeration.hasMoreElements()) {
++ list.add(enumeration.nextElement());
++ }
++ }
++
++ @Override
++ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
++ return this.loadClass(name, resolve, true, true);
++ }
++
++ @Override
++ public PluginMeta getConfiguration() {
++ return this.configuration;
++ }
++
++ @Override
++ public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException {
++ try {
++ Class<?> result = super.loadClass(name, resolve);
++
++ // SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins
++ if (checkGroup || result.getClassLoader() == this) {
++ return result;
++ }
++ } catch (ClassNotFoundException ignored) {
++ }
++
++ if (checkLibraries) {
++ try {
++ return this.libraryLoader.loadClass(name);
++ } catch (ClassNotFoundException ignored) {
++ }
++ }
++
++ if (checkGroup) {
++ // This ignores the libraries of other plugins, unless they are transitive dependencies.
++ if (this.group == null) {
++ throw new IllegalStateException("Tried to resolve class while group was not yet initialized");
++ }
++
++ Class<?> clazz = this.group.getClassByName(name, resolve, this);
++ if (clazz != null) {
++ return clazz;
++ }
++ }
++
++ throw new ClassNotFoundException(name);
++ }
++
++ @Override
++ public void init(JavaPlugin plugin) {
++ PluginMeta config = this.configuration;
++ PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile(
++ config.getName(),
++ config.getName(),
++ config.getProvidedPlugins(),
++ config.getMainClass(),
++ "", // Classloader load order api
++ config.getPluginDependencies(), // Dependencies
++ config.getPluginSoftDependencies(), // Soft Depends
++ config.getLoadBeforePlugins(), // Load Before
++ config.getVersion(),
++ Map.of(), // Commands, we use a separate system
++ config.getDescription(),
++ config.getAuthors(),
++ config.getContributors(),
++ config.getWebsite(),
++ config.getLoggerPrefix(),
++ config.getLoadOrder(),
++ config.getPermissions(),
++ config.getPermissionDefault(),
++ Set.of(), // Aware api
++ config.getAPIVersion(),
++ List.of() // Libraries
++ );
++
++ File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName());
++
++ plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config, this.logger);
++
++ this.loadedJavaPlugin = plugin;
++ }
++
++ @Nullable
++ @Override
++ public JavaPlugin getPlugin() {
++ return this.loadedJavaPlugin;
++ }
++
++ @Override
++ public String toString() {
++ return "PaperPluginClassLoader{" +
++ "libraryLoader=" + this.libraryLoader +
++ ", seenIllegalAccess=" + this.seenIllegalAccess +
++ ", loadedJavaPlugin=" + this.loadedJavaPlugin +
++ '}';
++ }
++
++ @Override
++ public void close() throws IOException {
++ try (this.jar; this.libraryLoader) {
++ super.close();
++ }
++ }
++
++ @Override
++ public @Nullable PluginClassLoaderGroup getGroup() {
++ return this.group;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..967465e542483e93a736129b5f5c6622cefd33fa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
+@@ -0,0 +1,116 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.util.NamespaceChecker;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.nio.file.Path;
++import java.security.CodeSigner;
++import java.security.CodeSource;
++import java.util.Enumeration;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++import java.util.jar.Manifest;
++
++/**
++ * Represents a simple classloader used for paper plugin bootstrappers.
++ */
++public class PaperSimplePluginClassLoader extends URLClassLoader {
++
++ static {
++ ClassLoader.registerAsParallelCapable();
++ }
++
++ protected final PluginMeta configuration;
++ protected final Path source;
++ protected final Manifest jarManifest;
++ protected final URL jarUrl;
++ protected final JarFile jar;
++
++ public PaperSimplePluginClassLoader(Path source, JarFile file, PluginMeta configuration, ClassLoader parentLoader) throws IOException {
++ super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader);
++
++ this.source = source;
++ this.jarManifest = file.getManifest();
++ this.jarUrl = source.toUri().toURL();
++ this.configuration = configuration;
++ this.jar = file;
++ }
++
++ @Override
++ public URL getResource(String name) {
++ return this.findResource(name);
++ }
++
++ @Override
++ public Enumeration<URL> getResources(String name) throws IOException {
++ return this.findResources(name);
++ }
++
++ // Bytecode modification supported loader
++ @Override
++ protected Class<?> findClass(String name) throws ClassNotFoundException {
++ NamespaceChecker.validateNameSpaceForClassloading(name);
++
++ // See UrlClassLoader#findClass(String)
++ String path = name.replace('.', '/').concat(".class");
++ JarEntry entry = this.jar.getJarEntry(path);
++ if (entry == null) {
++ throw new ClassNotFoundException(name);
++ }
++
++ // See URLClassLoader#defineClass(String, Resource)
++ byte[] classBytes;
++
++ try (InputStream is = this.jar.getInputStream(entry)) {
++ classBytes = is.readAllBytes();
++ } catch (IOException ex) {
++ throw new ClassNotFoundException(name, ex);
++ }
++
++ classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes);
++
++ int dot = name.lastIndexOf('.');
++ if (dot != -1) {
++ String pkgName = name.substring(0, dot);
++ // Get defined package does not correctly handle sealed packages.
++ if (this.getDefinedPackage(pkgName) == null) {
++ try {
++ if (this.jarManifest != null) {
++ this.definePackage(pkgName, this.jarManifest, this.jarUrl);
++ } else {
++ this.definePackage(pkgName, null, null, null, null, null, null, null);
++ }
++ } catch (IllegalArgumentException ex) {
++ // parallel-capable class loaders: re-verify in case of a
++ // race condition
++ if (this.getDefinedPackage(pkgName) == null) {
++ // Should never happen
++ throw new IllegalStateException("Cannot find package " + pkgName);
++ }
++ }
++ }
++ }
++
++ CodeSigner[] signers = entry.getCodeSigners();
++ CodeSource source = new CodeSource(this.jarUrl, signers);
++
++ return this.defineClass(name, classBytes, 0, classBytes.length, source);
++ }
++
++ @Override
++ public String toString() {
++ return "PaperSimplePluginClassLoader{" +
++ "configuration=" + this.configuration +
++ ", source=" + this.source +
++ ", jarManifest=" + this.jarManifest +
++ ", jarUrl=" + this.jarUrl +
++ ", jar=" + this.jar +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..eaf5c794cbe8d6138c9d60eaae20f5fc7711f541
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
+@@ -0,0 +1,47 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.ArrayList;
++
++public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++ private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup;
++ private final ClassLoaderAccess access;
++
++ public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) {
++ super(new ArrayList<>());
++ this.access = access;
++ this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup;
++ }
++
++ /**
++ * This will refresh the dependencies of the current classloader.
++ */
++ public void populateDependencies() {
++ this.classloaders.clear();
++ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) {
++ if (this.access.canAccess(configuredPluginClassLoader)) {
++ this.classloaders.add(configuredPluginClassLoader);
++ }
++ }
++
++ }
++
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return this.access;
++ }
++
++ @Override
++ public String toString() {
++ return "DependencyBasedPluginClassLoaderGroup{" +
++ "globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup +
++ ", access=" + this.access +
++ ", classloaders=" + this.classloaders +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2a4933088928a51c8135a3a60b7447d9d10c66c4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
+@@ -0,0 +1,18 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import org.jetbrains.annotations.ApiStatus;
++
++public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return (v) -> true;
++ }
++
++ @Override
++ public String toString() {
++ return "GLOBAL:" + super.toString();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aae50ebba6ba1579b75af5370c8b020d2a927b2c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
+@@ -0,0 +1,76 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.HashMap;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.concurrent.locks.ReentrantReadWriteLock;
++
++public class LockingClassLoaderGroup implements PluginClassLoaderGroup {
++
++ private final PluginClassLoaderGroup parent;
++ private final Map<String, ClassLockEntry> classLoadLock = new HashMap<>();
++
++ public LockingClassLoaderGroup(PluginClassLoaderGroup parent) {
++ this.parent = parent;
++ }
++
++ @Override
++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++ // make MT safe
++ ClassLockEntry lock;
++ synchronized (this.classLoadLock) {
++ lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock()));
++ lock.count.incrementAndGet();
++ }
++ lock.reentrantReadWriteLock.writeLock().lock();
++ try {
++ return parent.getClassByName(name, resolve, requester);
++ } finally {
++ synchronized (this.classLoadLock) {
++ lock.reentrantReadWriteLock.writeLock().unlock();
++ if (lock.count.get() == 1) {
++ this.classLoadLock.remove(name);
++ } else {
++ lock.count.decrementAndGet();
++ }
++ }
++ }
++ }
++
++ @Override
++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.parent.remove(configuredPluginClassLoader);
++ }
++
++ @Override
++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.parent.add(configuredPluginClassLoader);
++ }
++
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return this.parent.getAccess();
++ }
++
++ public PluginClassLoaderGroup getParent() {
++ return parent;
++ }
++
++ record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) {
++ }
++
++ @Override
++ public String toString() {
++ return "LockingClassLoaderGroup{" +
++ "parent=" + this.parent +
++ ", classLoadLock=" + this.classLoadLock +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e6fcdeb96356ff4713627b1458ac8bbfad1866b1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
+@@ -0,0 +1,93 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.CopyOnWriteArrayList;
++
++/**
++ * This is used for connecting multiple classloaders.
++ */
++public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage {
++
++ private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup();
++ private final List<PluginClassLoaderGroup> groups = new CopyOnWriteArrayList<>();
++
++ public PaperPluginClassLoaderStorage() {
++ this.groups.add(this.globalGroup);
++ }
++
++ @Override
++ public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) {
++ return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> {
++ return pluginClassLoader.dependencyContext.isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration());
++ }, pluginClassLoader));
++ }
++
++ @Override
++ public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) {
++ return this.registerGroup(classLoader, this.globalGroup);
++ }
++
++ @Override
++ public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) {
++ List<ConfiguredPluginClassLoader> allowedLoaders = new ArrayList<>();
++ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) {
++ if (access.canAccess(configuredPluginClassLoader)) {
++ allowedLoaders.add(configuredPluginClassLoader);
++ }
++ }
++
++ return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access, classLoader));
++ }
++
++ private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) {
++ // Now add this classloader to any groups that allows it (includes global)
++ for (PluginClassLoaderGroup loaderGroup : this.groups) {
++ if (loaderGroup.getAccess().canAccess(classLoader)) {
++ loaderGroup.add(classLoader);
++ }
++ }
++
++ group = new LockingClassLoaderGroup(group);
++ this.groups.add(group);
++ return group;
++ }
++
++ @Override
++ public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.globalGroup.remove(configuredPluginClassLoader);
++ this.groups.remove(configuredPluginClassLoader.getGroup());
++ for (PluginClassLoaderGroup group : this.groups) {
++ group.remove(configuredPluginClassLoader);
++ }
++ }
++
++ @Override
++ public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) {
++ if (this.globalGroup.getClassLoaders().contains(pluginLoader)) {
++ return false;
++ } else {
++ this.globalGroup.add(pluginLoader);
++ return true;
++ }
++ }
++
++ // Debug only
++ @ApiStatus.Internal
++ public GlobalPluginClassLoaderGroup getGlobalGroup() {
++ return this.globalGroup;
++ }
++
++ // Debug only
++ @ApiStatus.Internal
++ public List<PluginClassLoaderGroup> getGroups() {
++ return this.groups;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..23b6cb297f46c9c2b2944a3ab4031c31414620ad
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
+@@ -0,0 +1,69 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++import java.util.concurrent.CopyOnWriteArrayList;
++
++public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup {
++
++ private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization");
++
++ protected final List<ConfiguredPluginClassLoader> classloaders;
++
++ protected SimpleListPluginClassLoaderGroup() {
++ this(new CopyOnWriteArrayList<>());
++ }
++
++ protected SimpleListPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders) {
++ this.classloaders = classloaders;
++ }
++
++ @Override
++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++ if (!DISABLE_CLASS_PRIORITIZATION) {
++ try {
++ return this.lookupClass(name, false, requester); // First check the requester
++ } catch (ClassNotFoundException ignored) {
++ }
++ }
++
++ for (ConfiguredPluginClassLoader loader : this.classloaders) {
++ try {
++ return this.lookupClass(name, resolve, loader);
++ } catch (ClassNotFoundException ignored) {
++ }
++ }
++
++ return null;
++ }
++
++ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
++ return current.loadClass(name, resolve, false, true);
++ }
++
++ @Override
++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.classloaders.remove(configuredPluginClassLoader);
++ }
++
++ @Override
++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.classloaders.add(configuredPluginClassLoader);
++ }
++
++ public List<ConfiguredPluginClassLoader> getClassLoaders() {
++ return classloaders;
++ }
++
++ @Override
++ public String toString() {
++ return "SimpleListPluginClassLoaderGroup{" +
++ "classloaders=" + this.classloaders +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3b670bd6b35ae7f56488a9b50df54709a0b28901
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
+@@ -0,0 +1,60 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup {
++
++ private final ConfiguredPluginClassLoader configuredPluginClassLoader;
++ private final Access access;
++
++ public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ this.configuredPluginClassLoader = configuredPluginClassLoader;
++ this.access = new Access();
++ }
++
++ @Override
++ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++ try {
++ return this.configuredPluginClassLoader.loadClass(name, resolve, false, true);
++ } catch (ClassNotFoundException ignored) {
++ }
++
++ return null;
++ }
++
++ @Override
++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ }
++
++ @Override
++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ }
++
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return this.access;
++ }
++
++ @ApiStatus.Internal
++ private class Access implements ClassLoaderAccess {
++
++ @Override
++ public boolean canAccess(ConfiguredPluginClassLoader classLoader) {
++ return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader;
++ }
++
++ }
++
++ @Override
++ public String toString() {
++ return "SingletonPluginClassLoaderGroup{" +
++ "configuredPluginClassLoader=" + this.configuredPluginClassLoader +
++ ", access=" + this.access +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5a9f5b18e24a89dd642149a7a3d49390328b864b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.function.Predicate;
++
++/**
++ * Spigot classloaders have the ability to see everything.
++ * However, libraries are ONLY shared depending on their dependencies.
++ */
++public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++ private final Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate;
++ private final PluginClassLoader pluginClassLoader;
++
++ public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate, PluginClassLoader pluginClassLoader) {
++ super(globalPluginClassLoaderGroup.getClassLoaders());
++ this.libraryClassloaderPredicate = libraryClassloaderPredicate;
++ this.pluginClassLoader = pluginClassLoader;
++ }
++
++ // Mirrors global list
++ @Override
++ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ }
++
++ @Override
++ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ }
++
++ // Don't allow other plugins to access spigot dependencies, they should instead reference the global list
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return v -> false;
++ }
++
++ @Override
++ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
++ return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current));
++ }
++
++ // DEBUG
++ public PluginClassLoader getPluginClassLoader() {
++ return pluginClassLoader;
++ }
++
++ @Override
++ public String toString() {
++ return "SpigotPluginClassLoaderGroup{" +
++ "libraryClassloaderPredicate=" + this.libraryClassloaderPredicate +
++ ", classloaders=" + this.classloaders +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2412155ddfd559023f42ff534b8f06a52588e1e0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
+@@ -0,0 +1,40 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.List;
++
++public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++ private final ClassLoaderAccess access;
++ // Debug only
++ private final ConfiguredPluginClassLoader mainClassloaderHolder;
++
++ public StaticPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders, ClassLoaderAccess access, ConfiguredPluginClassLoader mainClassloaderHolder) {
++ super(classloaders);
++ this.access = access;
++ this.mainClassloaderHolder = mainClassloaderHolder;
++ }
++
++ @Override
++ public ClassLoaderAccess getAccess() {
++ return this.access;
++ }
++
++ // DEBUG
++ @ApiStatus.Internal
++ public ConfiguredPluginClassLoader getPluginClassloader() {
++ return this.mainClassloaderHolder;
++ }
++
++ @Override
++ public String toString() {
++ return "StaticPluginClassLoaderGroup{" +
++ "access=" + this.access +
++ ", classloaders=" + this.classloaders +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a32ed562f7fda4c2aeec5d1cc6188f21657edad0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/BootstrapMetaDependencyTree.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++
++public class BootstrapMetaDependencyTree extends MetaDependencyTree {
++ public BootstrapMetaDependencyTree() {
++ this(GraphBuilder.directed().build());
++ }
++
++ public BootstrapMetaDependencyTree(MutableGraph<String> graph) {
++ super(graph);
++ }
++
++ @Override
++ protected void registerDependencies(final String identifier, final PluginMeta meta) {
++ if (!(meta instanceof PaperPluginMeta paperPluginMeta)) {
++ throw new IllegalStateException("Only paper plugins can have a bootstrapper!");
++ }
++ // Build a validated provider's dependencies into the graph
++ for (String dependency : paperPluginMeta.getBootstrapDependencies().keySet()) {
++ this.graph.putEdge(identifier, dependency);
++ }
++ }
++
++ @Override
++ protected void unregisterDependencies(final String identifier, final PluginMeta meta) {
++ if (!(meta instanceof PaperPluginMeta paperPluginMeta)) {
++ throw new IllegalStateException("PluginMeta must be a PaperPluginMeta");
++ }
++
++ // Build a validated provider's dependencies into the graph
++ for (String dependency : paperPluginMeta.getBootstrapDependencies().keySet()) {
++ this.graph.removeEdge(identifier, dependency);
++ }
++ }
++
++ @Override
++ public String toString() {
++ return "BootstrapDependencyTree{" + "graph=" + this.graph + '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f43295fdeaa587cf30c35a1d545167071d58ce4b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
+@@ -0,0 +1,9 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++public interface DependencyContextHolder {
++
++ void setContext(DependencyContext context);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a2fa8406bc3f0dcab6805633ae984d031d24692a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
+@@ -0,0 +1,54 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.Graph;
++import com.google.common.graph.Graphs;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++import java.util.Set;
++
++@SuppressWarnings("UnstableApiUsage")
++public class GraphDependencyContext implements DependencyContext {
++
++ private final MutableGraph<String> dependencyGraph;
++
++ public GraphDependencyContext(MutableGraph<String> dependencyGraph) {
++ this.dependencyGraph = dependencyGraph;
++ }
++
++ @Override
++ public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) {
++ String pluginIdentifier = plugin.getName();
++
++ if (this.dependencyGraph.nodes().contains(pluginIdentifier)) {
++ Set<String> reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier);
++ if (reachableNodes.contains(depend.getName())) {
++ return true;
++ }
++ for (String provided : depend.getProvidedPlugins()) {
++ if (reachableNodes.contains(provided)) {
++ return true;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean hasDependency(String pluginIdentifier) {
++ return this.dependencyGraph.nodes().contains(pluginIdentifier);
++ }
++
++ public MutableGraph<String> getDependencyGraph() {
++ return dependencyGraph;
++ }
++
++ @Override
++ public String toString() {
++ return "GraphDependencyContext{" +
++ "dependencyGraph=" + this.dependencyGraph +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..461ef285764aac88ced32be5e650b48e8fbc6bfd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java
+@@ -0,0 +1,110 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.Graphs;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import org.jetbrains.annotations.NotNull;
++import java.util.HashSet;
++import java.util.Set;
++
++public abstract class MetaDependencyTree implements DependencyContext {
++
++ protected final MutableGraph<String> graph;
++
++ // We need to upkeep a separate collection since when populating
++ // a graph it adds nodes even if they are not present
++ protected final Set<String> dependencies = new HashSet<>();
++
++ public MetaDependencyTree() {
++ this(GraphBuilder.directed().build());
++ }
++
++ public MetaDependencyTree(MutableGraph<String> graph) {
++ this.graph = graph;
++ }
++
++ public void add(PluginMeta configuration) {
++ String identifier = configuration.getName();
++ // Build a validated provider's dependencies into the graph
++ this.registerDependencies(identifier, configuration);
++
++ this.graph.addNode(identifier); // Make sure dependencies at least have a node
++
++ // Add the provided plugins to the graph as well
++ for (String provides : configuration.getProvidedPlugins()) {
++ this.graph.putEdge(identifier, provides);
++ this.dependencies.add(provides);
++ }
++ this.dependencies.add(identifier);
++ }
++
++ protected abstract void registerDependencies(String identifier, PluginMeta meta);
++
++ public void remove(PluginMeta configuration) {
++ String identifier = configuration.getName();
++ // Remove a validated provider's dependencies into the graph
++ this.unregisterDependencies(identifier, configuration);
++
++ this.graph.removeNode(identifier); // Remove root node
++
++ // Remove the provided plugins to the graph as well
++ for (String provides : configuration.getProvidedPlugins()) {
++ this.graph.removeEdge(identifier, provides);
++ this.dependencies.remove(provides);
++ }
++ this.dependencies.remove(identifier);
++ }
++
++ protected abstract void unregisterDependencies(String identifier, PluginMeta meta);
++
++ @Override
++ public boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
++ String pluginIdentifier = plugin.getName();
++
++ if (this.graph.nodes().contains(pluginIdentifier)) {
++ Set<String> reachableNodes = Graphs.reachableNodes(this.graph, pluginIdentifier);
++ if (reachableNodes.contains(depend.getName())) {
++ return true;
++ }
++ for (String provided : depend.getProvidedPlugins()) {
++ if (reachableNodes.contains(provided)) {
++ return true;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ @Override
++ public boolean hasDependency(@NotNull String pluginIdentifier) {
++ return this.dependencies.contains(pluginIdentifier);
++ }
++
++ public void addDirectDependency(String dependency) {
++ this.dependencies.add(dependency);
++ }
++
++ @Override
++ public String toString() {
++ return "SimpleDependencyTree{" +
++ "graph=" + this.graph +
++ '}';
++ }
++
++ public MutableGraph<String> getGraph() {
++ return graph;
++ }
++
++ public void add(PluginProvider<?> provider) {
++ this.add(provider.getMeta());
++ }
++
++ public void remove(PluginProvider<?> provider) {
++ this.remove(provider.getMeta());
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..910caf78636769977dff55dd048c0150a36d218d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/SimpleMetaDependencyTree.java
+@@ -0,0 +1,40 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.Graphs;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.HashSet;
++import java.util.Set;
++
++public class SimpleMetaDependencyTree extends MetaDependencyTree {
++
++ public SimpleMetaDependencyTree() {
++ }
++
++ public SimpleMetaDependencyTree(final MutableGraph<String> graph) {
++ super(graph);
++ }
++
++ @Override
++ protected void registerDependencies(final String identifier, final PluginMeta meta) {
++ for (String dependency : meta.getPluginDependencies()) {
++ this.graph.putEdge(identifier, dependency);
++ }
++ for (String dependency : meta.getPluginSoftDependencies()) {
++ this.graph.putEdge(identifier, dependency);
++ }
++ }
++
++ @Override
++ protected void unregisterDependencies(final String identifier, final PluginMeta meta) {
++ for (String dependency : meta.getPluginDependencies()) {
++ this.graph.removeEdge(identifier, dependency);
++ }
++ for (String dependency : meta.getPluginSoftDependencies()) {
++ this.graph.removeEdge(identifier, dependency);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a9bca905eba67972e4d1b07b1d243272b62fec66
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
+@@ -0,0 +1,354 @@
++/*
++ * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors.
++ *
++ * JGraphT : a free Java graph-theory library
++ *
++ * See the CONTRIBUTORS.md file distributed with this work for additional
++ * information regarding copyright ownership.
++ *
++ * This program and the accompanying materials are made available under the
++ * terms of the Eclipse Public License 2.0 which is available at
++ * http://www.eclipse.org/legal/epl-2.0, or the
++ * GNU Lesser General Public License v2.1 or later
++ * which is available at
++ * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
++ *
++ * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
++ */
++
++// MODIFICATIONS:
++// - Modified to use a guava graph directly
++
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.base.Preconditions;
++import com.google.common.graph.Graph;
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import com.mojang.datafixers.util.Pair;
++
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.function.BiConsumer;
++import java.util.function.Consumer;
++
++/**
++ * Find all simple cycles of a directed graph using the Johnson's algorithm.
++ *
++ * <p>
++ * See:<br>
++ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975),
++ * pp. 77-84.
++ *
++ * @param <V> the vertex type.
++ *
++ * @author Nikolay Ognyanov
++ */
++public class JohnsonSimpleCycles<V>
++{
++ // The graph.
++ private Graph<V> graph;
++
++ // The main state of the algorithm.
++ private Consumer<List<V>> cycleConsumer = null;
++ private BiConsumer<V, V> cycleVertexSuccessorConsumer = null; // Paper
++ private V[] iToV = null;
++ private Map<V, Integer> vToI = null;
++ private Set<V> blocked = null;
++ private Map<V, Set<V>> bSets = null;
++ private ArrayDeque<V> stack = null;
++
++ // The state of the embedded Tarjan SCC algorithm.
++ private List<Set<V>> foundSCCs = null;
++ private int index = 0;
++ private Map<V, Integer> vIndex = null;
++ private Map<V, Integer> vLowlink = null;
++ private ArrayDeque<V> path = null;
++ private Set<V> pathSet = null;
++
++ /**
++ * Create a simple cycle finder for the specified graph.
++ *
++ * @param graph - the DirectedGraph in which to find cycles.
++ *
++ * @throws IllegalArgumentException if the graph argument is <code>
++ * null</code>.
++ */
++ public JohnsonSimpleCycles(Graph<V> graph)
++ {
++ Preconditions.checkState(graph.isDirected(), "Graph must be directed");
++ this.graph = graph;
++ }
++
++ /**
++ * Find the simple cycles of the graph.
++ *
++ * @return The list of all simple cycles. Possibly empty but never <code>null</code>.
++ */
++ public List<List<V>> findAndRemoveSimpleCycles()
++ {
++ List<List<V>> result = new ArrayList<>();
++ findSimpleCycles(result::add, (v, s) -> ((MutableGraph<V>) graph).removeEdge(v, s)); // Paper
++ return result;
++ }
++
++ /**
++ * Find the simple cycles of the graph.
++ *
++ * @param consumer Consumer that will be called with each cycle found.
++ */
++ public void findSimpleCycles(Consumer<List<V>> consumer, BiConsumer<V, V> vertexSuccessorConsumer) // Paper
++ {
++ if (graph == null) {
++ throw new IllegalArgumentException("Null graph.");
++ }
++ cycleVertexSuccessorConsumer = vertexSuccessorConsumer; // Paper
++ initState(consumer);
++
++ int startIndex = 0;
++ int size = graph.nodes().size();
++ while (startIndex < size) {
++ Pair<Graph<V>, Integer> minSCCGResult = findMinSCSG(startIndex);
++ if (minSCCGResult != null) {
++ startIndex = minSCCGResult.getSecond();
++ Graph<V> scg = minSCCGResult.getFirst();
++ V startV = toV(startIndex);
++ for (V v : scg.successors(startV)) {
++ blocked.remove(v);
++ getBSet(v).clear();
++ }
++ findCyclesInSCG(startIndex, startIndex, scg);
++ startIndex++;
++ } else {
++ break;
++ }
++ }
++
++ clearState();
++ }
++
++ private Pair<Graph<V>, Integer> findMinSCSG(int startIndex)
++ {
++ /*
++ * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph
++ * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected
++ * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum
++ * (among those SCCs) vertex index. We return that index together with the graph.
++ */
++ initMinSCGState();
++
++ List<Set<V>> foundSCCs = findSCCS(startIndex);
++
++ // find the SCC with the minimum index
++ int minIndexFound = Integer.MAX_VALUE;
++ Set<V> minSCC = null;
++ for (Set<V> scc : foundSCCs) {
++ for (V v : scc) {
++ int t = toI(v);
++ if (t < minIndexFound) {
++ minIndexFound = t;
++ minSCC = scc;
++ }
++ }
++ }
++ if (minSCC == null) {
++ return null;
++ }
++
++ // build a graph for the SCC found
++ MutableGraph<V> dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build();
++
++ for (V v : minSCC) {
++ for (V w : minSCC) {
++ if (graph.hasEdgeConnecting(v, w)) {
++ dependencyGraph.putEdge(v, w);
++ }
++ }
++ }
++
++ Pair<Graph<V>, Integer> result = Pair.of(dependencyGraph, minIndexFound);
++ clearMinSCCState();
++ return result;
++ }
++
++ private List<Set<V>> findSCCS(int startIndex)
++ {
++ // Find SCCs in the subgraph induced
++ // by vertices startIndex and beyond.
++ // A call to StrongConnectivityAlgorithm
++ // would be too expensive because of the
++ // need to materialize the subgraph.
++ // So - do a local search by the Tarjan's
++ // algorithm and pretend that vertices
++ // with an index smaller than startIndex
++ // do not exist.
++ for (V v : graph.nodes()) {
++ int vI = toI(v);
++ if (vI < startIndex) {
++ continue;
++ }
++ if (!vIndex.containsKey(v)) {
++ getSCCs(startIndex, vI);
++ }
++ }
++ List<Set<V>> result = foundSCCs;
++ foundSCCs = null;
++ return result;
++ }
++
++ private void getSCCs(int startIndex, int vertexIndex)
++ {
++ V vertex = toV(vertexIndex);
++ vIndex.put(vertex, index);
++ vLowlink.put(vertex, index);
++ index++;
++ path.push(vertex);
++ pathSet.add(vertex);
++
++ Set<V> edges = graph.successors(vertex);
++ for (V successor : edges) {
++ int successorIndex = toI(successor);
++ if (successorIndex < startIndex) {
++ continue;
++ }
++ if (!vIndex.containsKey(successor)) {
++ getSCCs(startIndex, successorIndex);
++ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor)));
++ } else if (pathSet.contains(successor)) {
++ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor)));
++ }
++ }
++ if (vLowlink.get(vertex).equals(vIndex.get(vertex))) {
++ Set<V> result = new HashSet<>();
++ V temp;
++ do {
++ temp = path.pop();
++ pathSet.remove(temp);
++ result.add(temp);
++ } while (!vertex.equals(temp));
++ if (result.size() == 1) {
++ V v = result.iterator().next();
++ if (graph.edges().contains(vertex)) {
++ foundSCCs.add(result);
++ }
++ } else {
++ foundSCCs.add(result);
++ }
++ }
++ }
++
++ private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph<V> scg)
++ {
++ /*
++ * Find cycles in a strongly connected graph per Johnson.
++ */
++ boolean foundCycle = false;
++ V vertex = toV(vertexIndex);
++ stack.push(vertex);
++ blocked.add(vertex);
++
++ for (V successor : scg.successors(vertex)) {
++ int successorIndex = toI(successor);
++ if (successorIndex == startIndex) {
++ List<V> cycle = new ArrayList<>(stack.size());
++ stack.descendingIterator().forEachRemaining(cycle::add);
++ cycleConsumer.accept(cycle);
++ cycleVertexSuccessorConsumer.accept(vertex, successor); // Paper
++ //foundCycle = true; // Paper
++ } else if (!blocked.contains(successor)) {
++ boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
++ foundCycle = foundCycle || gotCycle;
++ }
++ }
++ if (foundCycle) {
++ unblock(vertex);
++ } else {
++ for (V w : scg.successors(vertex)) {
++ Set<V> bSet = getBSet(w);
++ bSet.add(vertex);
++ }
++ }
++ stack.pop();
++ return foundCycle;
++ }
++
++ private void unblock(V vertex)
++ {
++ blocked.remove(vertex);
++ Set<V> bSet = getBSet(vertex);
++ while (bSet.size() > 0) {
++ V w = bSet.iterator().next();
++ bSet.remove(w);
++ if (blocked.contains(w)) {
++ unblock(w);
++ }
++ }
++ }
++
++ @SuppressWarnings("unchecked")
++ private void initState(Consumer<List<V>> consumer)
++ {
++ cycleConsumer = consumer;
++ iToV = (V[]) graph.nodes().toArray();
++ vToI = new HashMap<>();
++ blocked = new HashSet<>();
++ bSets = new HashMap<>();
++ stack = new ArrayDeque<>();
++
++ for (int i = 0; i < iToV.length; i++) {
++ vToI.put(iToV[i], i);
++ }
++ }
++
++ private void clearState()
++ {
++ cycleConsumer = null;
++ iToV = null;
++ vToI = null;
++ blocked = null;
++ bSets = null;
++ stack = null;
++ }
++
++ private void initMinSCGState()
++ {
++ index = 0;
++ foundSCCs = new ArrayList<>();
++ vIndex = new HashMap<>();
++ vLowlink = new HashMap<>();
++ path = new ArrayDeque<>();
++ pathSet = new HashSet<>();
++ }
++
++ private void clearMinSCCState()
++ {
++ index = 0;
++ foundSCCs = null;
++ vIndex = null;
++ vLowlink = null;
++ path = null;
++ pathSet = null;
++ }
++
++ private Integer toI(V vertex)
++ {
++ return vToI.get(vertex);
++ }
++
++ private V toV(Integer i)
++ {
++ return iToV[i];
++ }
++
++ private Set<V> getBSet(V v)
++ {
++ // B sets typically not all needed,
++ // so instantiate lazily.
++ return bSets.computeIfAbsent(v, k -> new HashSet<>());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f59f48654eaa299bcac862991b1e2e622264639b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
+@@ -0,0 +1,271 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import org.bukkit.plugin.UnknownDependencyException;
++
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.LinkedList;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++@SuppressWarnings("UnstableApiUsage")
++public class LegacyPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
++
++ private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy");
++ private final ProviderConfiguration<T> configuration;
++
++ public LegacyPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
++ this.configuration = onLoad;
++ }
++
++ @Override
++ public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> providers, MetaDependencyTree dependencyTree) {
++ List<ProviderPair<T>> javapluginsLoaded = new ArrayList<>();
++ MutableGraph<String> dependencyGraph = dependencyTree.getGraph();
++
++ Map<String, PluginProvider<T>> providersToLoad = new HashMap<>();
++ Set<String> loadedPlugins = new HashSet<>();
++ Map<String, String> pluginsProvided = new HashMap<>();
++ Map<String, Collection<String>> dependencies = new HashMap<>();
++ Map<String, Collection<String>> softDependencies = new HashMap<>();
++
++ for (PluginProvider<T> provider : providers) {
++ PluginMeta configuration = provider.getMeta();
++
++ PluginProvider<T> replacedProvider = providersToLoad.put(configuration.getName(), provider);
++ dependencyTree.addDirectDependency(configuration.getName()); // add to dependency tree
++ if (replacedProvider != null) {
++ LOGGER.severe(String.format(
++ "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
++ configuration.getName(),
++ provider.getSource(),
++ replacedProvider.getSource(),
++ replacedProvider.getParentSource()
++ ));
++ }
++
++ String removedProvided = pluginsProvided.remove(configuration.getName());
++ if (removedProvided != null) {
++ LOGGER.warning(String.format(
++ "Ambiguous plugin name `%s'. It is also provided by `%s'",
++ configuration.getName(),
++ removedProvided
++ ));
++ }
++
++ for (String provided : configuration.getProvidedPlugins()) {
++ PluginProvider<T> pluginProvider = providersToLoad.get(provided);
++
++ if (pluginProvider != null) {
++ LOGGER.warning(String.format(
++ "`%s provides `%s' while this is also the name of `%s' in `%s'",
++ provider.getSource(),
++ provided,
++ pluginProvider.getSource(),
++ provider.getParentSource()
++ ));
++ } else {
++ String replacedPlugin = pluginsProvided.put(provided, configuration.getName());
++ dependencyTree.addDirectDependency(provided); // add to dependency tree
++ if (replacedPlugin != null) {
++ LOGGER.warning(String.format(
++ "`%s' is provided by both `%s' and `%s'",
++ provided,
++ configuration.getName(),
++ replacedPlugin
++ ));
++ }
++ }
++ }
++
++ Collection<String> softDependencySet = provider.getMeta().getPluginSoftDependencies();
++ if (softDependencySet != null && !softDependencySet.isEmpty()) {
++ if (softDependencies.containsKey(configuration.getName())) {
++ // Duplicates do not matter, they will be removed together if applicable
++ softDependencies.get(configuration.getName()).addAll(softDependencySet);
++ } else {
++ softDependencies.put(configuration.getName(), new LinkedList<String>(softDependencySet));
++ }
++
++ for (String depend : softDependencySet) {
++ dependencyGraph.putEdge(configuration.getName(), depend);
++ }
++ }
++
++ Collection<String> dependencySet = provider.getMeta().getPluginDependencies();
++ if (dependencySet != null && !dependencySet.isEmpty()) {
++ dependencies.put(configuration.getName(), new LinkedList<String>(dependencySet));
++
++ for (String depend : dependencySet) {
++ dependencyGraph.putEdge(configuration.getName(), depend);
++ }
++ }
++
++ Collection<String> loadBeforeSet = provider.getMeta().getLoadBeforePlugins();
++ if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
++ for (String loadBeforeTarget : loadBeforeSet) {
++ if (softDependencies.containsKey(loadBeforeTarget)) {
++ softDependencies.get(loadBeforeTarget).add(configuration.getName());
++ } else {
++ // softDependencies is never iterated, so 'ghost' plugins aren't an issue
++ Collection<String> shortSoftDependency = new LinkedList<String>();
++ shortSoftDependency.add(configuration.getName());
++ softDependencies.put(loadBeforeTarget, shortSoftDependency);
++ }
++
++ dependencyGraph.putEdge(loadBeforeTarget, configuration.getName());
++ }
++ }
++ }
++
++ while (!providersToLoad.isEmpty()) {
++ boolean missingDependency = true;
++ Iterator<Map.Entry<String, PluginProvider<T>>> providerIterator = providersToLoad.entrySet().iterator();
++
++ while (providerIterator.hasNext()) {
++ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
++ String providerIdentifier = entry.getKey();
++
++ if (dependencies.containsKey(providerIdentifier)) {
++ Iterator<String> dependencyIterator = dependencies.get(providerIdentifier).iterator();
++ final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends
++
++ while (dependencyIterator.hasNext()) {
++ String dependency = dependencyIterator.next();
++
++ // Dependency loaded
++ if (loadedPlugins.contains(dependency)) {
++ dependencyIterator.remove();
++
++ // We have a dependency not found
++ } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
++ // Paper start
++ missingHardDependencies.add(dependency);
++ }
++ }
++ if (!missingHardDependencies.isEmpty()) {
++ // Paper end
++ missingDependency = false;
++ providerIterator.remove();
++ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
++ softDependencies.remove(providerIdentifier);
++ dependencies.remove(providerIdentifier);
++
++ LOGGER.log(
++ Level.SEVERE,
++ "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper
++ new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper
++ }
++
++ if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) {
++ dependencies.remove(providerIdentifier);
++ }
++ }
++ if (softDependencies.containsKey(providerIdentifier)) {
++ Iterator<String> softDependencyIterator = softDependencies.get(providerIdentifier).iterator();
++
++ while (softDependencyIterator.hasNext()) {
++ String softDependency = softDependencyIterator.next();
++
++ // Soft depend is no longer around
++ if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) {
++ softDependencyIterator.remove();
++ }
++ }
++
++ if (softDependencies.get(providerIdentifier).isEmpty()) {
++ softDependencies.remove(providerIdentifier);
++ }
++ }
++ if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) {
++ // We're clear to load, no more soft or hard dependencies left
++ PluginProvider<T> file = providersToLoad.get(providerIdentifier);
++ providerIterator.remove();
++ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
++ missingDependency = false;
++
++ try {
++ this.configuration.applyContext(file, dependencyTree);
++ T loadedPlugin = file.createInstance();
++ this.warnIfPaperPlugin(file);
++
++ if (this.configuration.load(file, loadedPlugin)) {
++ loadedPlugins.add(file.getMeta().getName());
++ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
++ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
++ }
++
++ } catch (Throwable ex) {
++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
++ }
++ }
++ }
++
++ if (missingDependency) {
++ // We now iterate over plugins until something loads
++ // This loop will ignore soft dependencies
++ providerIterator = providersToLoad.entrySet().iterator();
++
++ while (providerIterator.hasNext()) {
++ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
++ String plugin = entry.getKey();
++
++ if (!dependencies.containsKey(plugin)) {
++ softDependencies.remove(plugin);
++ missingDependency = false;
++ PluginProvider<T> file = entry.getValue();
++ providerIterator.remove();
++
++ try {
++ this.configuration.applyContext(file, dependencyTree);
++ T loadedPlugin = file.createInstance();
++ this.warnIfPaperPlugin(file);
++
++ if (this.configuration.load(file, loadedPlugin)) {
++ loadedPlugins.add(file.getMeta().getName());
++ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
++ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
++ }
++ break;
++ } catch (Throwable ex) {
++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
++ }
++ }
++ }
++ // We have no plugins left without a depend
++ if (missingDependency) {
++ softDependencies.clear();
++ dependencies.clear();
++ Iterator<PluginProvider<T>> failedPluginIterator = providersToLoad.values().iterator();
++
++ while (failedPluginIterator.hasNext()) {
++ PluginProvider<T> file = failedPluginIterator.next();
++ failedPluginIterator.remove();
++ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper
++ }
++ }
++ }
++ }
++
++ return javapluginsLoaded;
++ }
++
++ private void warnIfPaperPlugin(PluginProvider<T> provider) {
++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++ provider.getLogger().warn("Loading Paper plugin in the legacy plugin loading logic. This is not recommended and may introduce some differences into load order. It's highly recommended you move away from this if you are wanting to use Paper plugins.");
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2ea978ac957849260e7ca69c9ff56588d0ccc41b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
+@@ -0,0 +1,19 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import java.util.List;
++
++/**
++ * Indicates a dependency cycle within a provider loading sequence.
++ */
++public class PluginGraphCycleException extends RuntimeException {
++
++ private final List<List<String>> cycles;
++
++ public PluginGraphCycleException(List<List<String>> cycles) {
++ this.cycles = cycles;
++ }
++
++ public List<List<String>> getCycles() {
++ return this.cycles;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..67c4ef672ee509deba2b4bcaac42d9db24d4c89a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++/**
++ * Used to share code with the modern and legacy plugin load strategy.
++ *
++ * @param <T>
++ */
++public interface ProviderConfiguration<T> {
++
++ void applyContext(PluginProvider<T> provider, DependencyContext dependencyContext);
++
++ boolean load(PluginProvider<T> provider, T provided);
++
++ default boolean preloadProvider(PluginProvider<T> provider) {
++ return true;
++ }
++
++ default void onGenericError(PluginProvider<T> provider) {
++
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a79eb9e2c8c42ecf823aecbd859576415e9981dc
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
+@@ -0,0 +1,22 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++import java.util.List;
++
++/**
++ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
++ * <p>
++ * Returns providers loaded.
++ *
++ * @param <P> provider type
++ */
++public interface ProviderLoadingStrategy<P> {
++
++ List<ProviderPair<P>> loadProviders(List<PluginProvider<P>> providers, MetaDependencyTree dependencyTree);
++
++ record ProviderPair<P>(PluginProvider<P> provider, P provided) {
++
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..52a110044611c8a0ace6d49549e8acc16cbbe83d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.graph.Graph;
++import it.unimi.dsi.fastutil.objects.Object2IntMap;
++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.Deque;
++import java.util.List;
++
++public final class TopographicGraphSorter {
++
++ // Topographically sort dependencies
++ public static <N> List<N> sortGraph(Graph<N> graph) throws PluginGraphCycleException {
++ List<N> sorted = new ArrayList<>();
++ Deque<N> roots = new ArrayDeque<>();
++ Object2IntMap<N> nonRoots = new Object2IntOpenHashMap<>();
++
++ for (N node : graph.nodes()) {
++ // Is a node being referred to by any other nodes?
++ int degree = graph.inDegree(node);
++ if (degree == 0) {
++ // Is a root
++ roots.add(node);
++ } else {
++ // Isn't a root, the number represents how many nodes connect to it.
++ nonRoots.put(node, degree);
++ }
++ }
++
++ // Pick from nodes that aren't referred to anywhere else
++ N next;
++ while ((next = roots.poll()) != null) {
++ for (N successor : graph.successors(next)) {
++ // Traverse through, moving down a degree
++ int newInDegree = nonRoots.removeInt(successor) - 1;
++
++ if (newInDegree == 0) {
++ roots.add(successor);
++ } else {
++ nonRoots.put(successor, newInDegree);
++ }
++
++ }
++ sorted.add(next);
++ }
++
++ if (!nonRoots.isEmpty()) {
++ throw new GraphCycleException();
++ }
++
++ return sorted;
++ }
++
++ public static final class GraphCycleException extends RuntimeException {
++
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e3f01ec40a704acb46f7ac31d500e9d0185e3db9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java
+@@ -0,0 +1,121 @@
++package io.papermc.paper.plugin.entrypoint.strategy.modern;
++
++import com.google.common.collect.Lists;
++import com.google.common.graph.MutableGraph;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.strategy.JohnsonSimpleCycles;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.TopographicGraphSorter;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++
++class LoadOrderTree {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final Map<String, PluginProvider<?>> providerMap;
++ private final MutableGraph<String> graph;
++
++ public LoadOrderTree(Map<String, PluginProvider<?>> providerMapMirror, MutableGraph<String> graph) {
++ this.providerMap = providerMapMirror;
++ this.graph = graph;
++ }
++
++ public void add(PluginProvider<?> provider) {
++ LoadOrderConfiguration configuration = provider.createConfiguration(this.providerMap);
++
++ // Build a validated provider's load order changes
++ String identifier = configuration.getMeta().getName();
++ for (String dependency : configuration.getLoadAfter()) {
++ if (this.providerMap.containsKey(dependency)) {
++ this.graph.putEdge(identifier, dependency);
++ }
++ }
++
++ for (String loadBeforeTarget : configuration.getLoadBefore()) {
++ if (this.providerMap.containsKey(loadBeforeTarget)) {
++ this.graph.putEdge(loadBeforeTarget, identifier);
++ }
++ }
++
++ this.graph.addNode(identifier); // Make sure load order has at least one node
++ }
++
++ public List<String> getLoadOrder() throws PluginGraphCycleException {
++ List<String> reversedTopographicSort;
++ try {
++ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
++ } catch (TopographicGraphSorter.GraphCycleException exception) {
++ List<List<String>> cycles = new JohnsonSimpleCycles<>(this.graph).findAndRemoveSimpleCycles();
++
++ // Only log an error if at least non-Spigot plugin is present in the cycle
++ // Due to Spigot plugin metadata making no distinction between load order and dependencies (= class loader access), cycles are an unfortunate reality we have to deal with
++ Set<String> cyclingPlugins = new HashSet<>();
++ cycles.forEach(cyclingPlugins::addAll);
++ if (cyclingPlugins.stream().anyMatch(plugin -> {
++ PluginProvider<?> pluginProvider = this.providerMap.get(plugin);
++ return pluginProvider != null && !(pluginProvider instanceof SpigotPluginProvider);
++ })) {
++ logCycleError(cycles, this.providerMap);
++ }
++
++ // Try again after hopefully having removed all cycles
++ try {
++ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
++ } catch (TopographicGraphSorter.GraphCycleException e) {
++ throw new PluginGraphCycleException(cycles);
++ }
++ }
++
++ return reversedTopographicSort;
++ }
++
++ private void logCycleError(List<List<String>> cycles, Map<String, PluginProvider<?>> providerMapMirror) {
++ LOGGER.error("=================================");
++ LOGGER.error("Circular plugin loading detected:");
++ for (int i = 0; i < cycles.size(); i++) {
++ List<String> cycle = cycles.get(i);
++ LOGGER.error("{}) {} -> {}", i + 1, String.join(" -> ", cycle), cycle.get(0));
++ for (String pluginName : cycle) {
++ PluginProvider<?> pluginProvider = providerMapMirror.get(pluginName);
++ if (pluginProvider == null) {
++ return;
++ }
++
++ logPluginInfo(pluginProvider.getMeta());
++ }
++ }
++
++ LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
++ LOGGER.error("=================================");
++ }
++
++ private void logPluginInfo(PluginMeta meta) {
++ if (!meta.getLoadBeforePlugins().isEmpty()) {
++ LOGGER.error(" {} loadbefore: {}", meta.getName(), meta.getLoadBeforePlugins());
++ }
++
++ if (meta instanceof PaperPluginMeta paperPluginMeta) {
++ if (!paperPluginMeta.getLoadAfterPlugins().isEmpty()) {
++ LOGGER.error(" {} loadafter: {}", meta.getName(), paperPluginMeta.getLoadAfterPlugins());
++ }
++ } else {
++ List<String> dependencies = new ArrayList<>();
++ dependencies.addAll(meta.getPluginDependencies());
++ dependencies.addAll(meta.getPluginSoftDependencies());
++ if (!dependencies.isEmpty()) {
++ LOGGER.error(" {} depend/softdepend: {}", meta.getName(), dependencies);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9af388a8e56806ab44f8c3ef4f97086ce38ef3b4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java
+@@ -0,0 +1,138 @@
++package io.papermc.paper.plugin.entrypoint.strategy.modern;
++
++import com.google.common.collect.Maps;
++import com.google.common.graph.GraphBuilder;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++
++@SuppressWarnings("UnstableApiUsage")
++public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private final ProviderConfiguration<T> configuration;
++
++ public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
++ this.configuration = onLoad;
++ }
++
++ @Override
++ public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> pluginProviders, MetaDependencyTree dependencyTree) {
++ Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
++ Map<String, PluginProvider<?>> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider);
++ List<PluginProvider<T>> validatedProviders = new ArrayList<>();
++
++ // Populate provider map
++ for (PluginProvider<T> provider : pluginProviders) {
++ PluginMeta providerConfig = provider.getMeta();
++ PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
++
++ PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
++ if (replacedProvider != null) {
++ LOGGER.error(String.format(
++ "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
++ providerConfig.getName(),
++ provider.getSource(),
++ replacedProvider.provider.getSource(),
++ replacedProvider.provider.getParentSource()
++ ));
++ this.configuration.onGenericError(replacedProvider.provider);
++ }
++
++ for (String extra : providerConfig.getProvidedPlugins()) {
++ PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
++ if (replacedExtraProvider != null) {
++ LOGGER.warn(String.format(
++ "`%s' is provided by both `%s' and `%s'",
++ extra,
++ providerConfig.getName(),
++ replacedExtraProvider.provider.getMeta().getName()
++ ));
++ }
++ }
++ }
++
++ // Populate dependency tree
++ for (PluginProvider<?> validated : pluginProviders) {
++ dependencyTree.add(validated);
++ }
++
++ // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
++ for (PluginProvider<T> provider : pluginProviders) {
++ PluginMeta configuration = provider.getMeta();
++
++ // Populate missing dependencies to capture if there are multiple missing ones.
++ List<String> missingDependencies = provider.validateDependencies(dependencyTree);
++
++ if (missingDependencies.isEmpty()) {
++ validatedProviders.add(provider);
++ } else {
++ LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
++ // Because the validator is invalid, remove it from the provider map
++ providerMap.remove(configuration.getName());
++ // Cleanup plugins that failed to load
++ dependencyTree.remove(provider);
++ this.configuration.onGenericError(provider);
++ }
++ }
++
++ LoadOrderTree loadOrderTree = new LoadOrderTree(providerMapMirror, GraphBuilder.directed().build());
++ // Populate load order tree
++ for (PluginProvider<?> validated : validatedProviders) {
++ loadOrderTree.add(validated);
++ }
++
++ // Reverse the topographic search to let us see which providers we can load first.
++ List<String> reversedTopographicSort = loadOrderTree.getLoadOrder();
++ List<ProviderPair<T>> loadedPlugins = new ArrayList<>();
++ for (String providerIdentifier : reversedTopographicSort) {
++ // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
++ // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
++ // nicer to do it here TBH.
++ PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
++ if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
++ // OR if this was already provided (most likely from a plugin that already "provides" that dependency)
++ // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
++ continue; // Skip provider that doesn't exist....
++ }
++ retrievedProviderEntry.provided = true;
++ PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
++ try {
++ this.configuration.applyContext(retrievedProvider, dependencyTree);
++
++ if (this.configuration.preloadProvider(retrievedProvider)) {
++ T instance = retrievedProvider.createInstance();
++ if (this.configuration.load(retrievedProvider, instance)) {
++ loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance));
++ }
++ }
++ } catch (Throwable ex) {
++ LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
++ }
++ }
++
++ return loadedPlugins;
++ }
++
++ private static class PluginProviderEntry<T> {
++
++ private final PluginProvider<T> provider;
++ private boolean provided;
++
++ private PluginProviderEntry(PluginProvider<T> provider) {
++ this.provider = provider;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f38ecd7f65dc24e4a3f0bc675e3730287ac353f1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -0,0 +1,64 @@
++package io.papermc.paper.plugin.loader;
++
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
++import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.IOException;
++import java.net.MalformedURLException;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++public class PaperClasspathBuilder implements PluginClasspathBuilder {
++
++ private final List<ClassPathLibrary> libraries = new ArrayList<>();
++
++ private final PluginProviderContext context;
++
++ public PaperClasspathBuilder(PluginProviderContext context) {
++ this.context = context;
++ }
++
++ @Override
++ public @NotNull PluginProviderContext getContext() {
++ return this.context;
++ }
++
++ @Override
++ public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) {
++ this.libraries.add(classPathLibrary);
++ return this;
++ }
++
++ public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) {
++ PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
++ for (ClassPathLibrary library : this.libraries) {
++ library.register(paperLibraryStore);
++ }
++
++ List<Path> paths = paperLibraryStore.getPaths();
++ URL[] urls = new URL[paths.size()];
++ for (int i = 0; i < paths.size(); i++) {
++ Path path = paperLibraryStore.getPaths().get(i);
++ try {
++ urls[i] = path.toUri().toURL();
++ } catch (MalformedURLException e) {
++ throw new AssertionError(e);
++ }
++ }
++
++ try {
++ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
++ } catch (IOException exception) {
++ throw new RuntimeException(exception);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5fcce65009f715d46dd3013f1f92ec8393d66e15
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.plugin.loader.library;
++
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++
++public class PaperLibraryStore implements LibraryStore {
++
++ private final List<Path> paths = new ArrayList<>();
++
++ @Override
++ public void addLibrary(@NotNull Path library) {
++ this.paths.add(library);
++ }
++
++ public List<Path> getPaths() {
++ return this.paths;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aef19b44075a3b2e8696315baa89117dd8ebb513
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java
+@@ -0,0 +1,82 @@
++package io.papermc.paper.plugin.manager;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginFileType;
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.event.Listener;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.RegisteredListener;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.File;
++import java.io.FileNotFoundException;
++import java.io.IOException;
++import java.util.Map;
++import java.util.Set;
++import java.util.jar.JarFile;
++import java.util.regex.Pattern;
++
++/**
++ * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation
++ * of papers new plugin system.
++ */
++public class DummyBukkitPluginLoader implements PluginLoader {
++
++ private static final Pattern[] PATTERNS = new Pattern[0];
++
++ @Override
++ public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
++ try {
++ return PaperPluginManagerImpl.getInstance().loadPlugin(file);
++ } catch (InvalidDescriptionException e) {
++ throw new InvalidPluginException(e);
++ }
++ }
++
++ @Override
++ public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
++ try (JarFile jar = new JarFile(file)) {
++ PluginFileType<?, ?> type = PluginFileType.guessType(jar);
++ if (type == null) {
++ throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml"));
++ }
++
++ PluginMeta meta = type.getConfig(jar);
++ if (meta instanceof PluginDescriptionFile pluginDescriptionFile) {
++ return pluginDescriptionFile;
++ } else {
++ throw new InvalidDescriptionException("Plugin type does not use plugin.yml. Cannot read file description.");
++ }
++ } catch (Exception e) {
++ throw new InvalidDescriptionException(e);
++ }
++ }
++
++ @Override
++ public @NotNull Pattern[] getPluginFileFilters() {
++ return PATTERNS;
++ }
++
++ @Override
++ public @NotNull Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) {
++ return PaperPluginManagerImpl.getInstance().paperEventManager.createRegisteredListeners(listener, plugin);
++ }
++
++ @Override
++ public void enablePlugin(@NotNull Plugin plugin) {
++ Bukkit.getPluginManager().enablePlugin(plugin);
++ }
++
++ @Override
++ public void disablePlugin(@NotNull Plugin plugin) {
++ Bukkit.getPluginManager().disablePlugin(plugin);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d681222f355af5c4c26f35aaba484a393aee41c6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
+@@ -0,0 +1,61 @@
++package io.papermc.paper.plugin.manager;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private final List<JavaPlugin> provided = new ArrayList<>();
++
++ private final MetaDependencyTree dependencyTree;
++
++ MultiRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
++ this.dependencyTree = dependencyTree;
++ }
++
++ @Override
++ public void register(PluginProvider<JavaPlugin> provider) {
++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++ LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager.");
++ return;
++ }
++ super.register(provider);
++ /*
++ Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage,
++ as putting it into the entrypoint handlers doesn't make much sense.
++ */
++ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
++ }
++
++ @Override
++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
++ super.processProvided(provider, provided);
++ this.provided.add(provided);
++ }
++
++ @Override
++ public boolean throwOnCycle() {
++ return false;
++ }
++
++ public List<JavaPlugin> getLoaded() {
++ return this.provided;
++ }
++
++ @Override
++ public MetaDependencyTree createDependencyTree() {
++ return this.dependencyTree;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6f6aaab295018017565ba27d6958a1f5c7b69bc8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
+@@ -0,0 +1,43 @@
++package io.papermc.paper.plugin.manager;
++
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++
++import java.util.HashMap;
++import java.util.LinkedHashMap;
++import java.util.LinkedHashSet;
++import java.util.Map;
++import java.util.Set;
++
++class NormalPaperPermissionManager extends PaperPermissionManager {
++
++ private final Map<String, Permission> permissions = new HashMap<>();
++ private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<>();
++ private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<>();
++ private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<>();
++
++ public NormalPaperPermissionManager() {
++ this.defaultPerms().put(true, new LinkedHashSet<>());
++ this.defaultPerms().put(false, new LinkedHashSet<>());
++ }
++
++ @Override
++ public Map<String, Permission> permissions() {
++ return this.permissions;
++ }
++
++ @Override
++ public Map<Boolean, Set<Permission>> defaultPerms() {
++ return this.defaultPerms;
++ }
++
++ @Override
++ public Map<String, Map<Permissible, Boolean>> permSubs() {
++ return this.permSubs;
++ }
++
++ @Override
++ public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
++ return this.defSubs;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+@@ -0,0 +1,194 @@
++package io.papermc.paper.plugin.manager;
++
++import co.aikar.timings.TimedEventExecutor;
++import com.destroystokyo.paper.event.server.ServerExceptionEvent;
++import com.destroystokyo.paper.exception.ServerEventException;
++import com.google.common.collect.Sets;
++import org.bukkit.Server;
++import org.bukkit.Warning;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventHandler;
++import org.bukkit.event.EventPriority;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.Listener;
++import org.bukkit.plugin.AuthorNagException;
++import org.bukkit.plugin.EventExecutor;
++import org.bukkit.plugin.IllegalPluginAccessException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.RegisteredListener;
++import org.jetbrains.annotations.NotNull;
++
++import java.lang.reflect.Method;
++import java.util.Arrays;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Map;
++import java.util.Set;
++import java.util.logging.Level;
++
++class PaperEventManager {
++
++ private final Server server;
++
++ public PaperEventManager(Server server) {
++ this.server = server;
++ }
++
++ // SimplePluginManager
++ public void callEvent(@NotNull Event event) {
++ if (event.isAsynchronous() && this.server.isPrimaryThread()) {
++ throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
++ } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) {
++ throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
++ }
++
++ HandlerList handlers = event.getHandlers();
++ RegisteredListener[] listeners = handlers.getRegisteredListeners();
++
++ for (RegisteredListener registration : listeners) {
++ if (!registration.getPlugin().isEnabled()) {
++ continue;
++ }
++
++ try {
++ registration.callEvent(event);
++ } catch (AuthorNagException ex) {
++ Plugin plugin = registration.getPlugin();
++
++ if (plugin.isNaggable()) {
++ plugin.setNaggable(false);
++
++ this.server.getLogger().log(Level.SEVERE, String.format(
++ "Nag author(s): '%s' of '%s' about the following: %s",
++ plugin.getPluginMeta().getAuthors(),
++ plugin.getPluginMeta().getDisplayName(),
++ ex.getMessage()
++ ));
++ }
++ } catch (Throwable ex) {
++ String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName();
++ this.server.getLogger().log(Level.SEVERE, msg, ex);
++ if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
++ this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
++ }
++ }
++ }
++ }
++
++ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
++ if (!plugin.isEnabled()) {
++ throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
++ }
++
++ for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
++ this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
++ }
++
++ }
++
++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
++ this.registerEvent(event, listener, priority, executor, plugin, false);
++ }
++
++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
++ if (!plugin.isEnabled()) {
++ throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
++ }
++
++ executor = new TimedEventExecutor(executor, plugin, null, event);
++ this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
++ }
++
++ @NotNull
++ private HandlerList getEventListeners(@NotNull Class<? extends Event> type) {
++ try {
++ Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList");
++ method.setAccessible(true);
++ return (HandlerList) method.invoke(null);
++ } catch (Exception e) {
++ throw new IllegalPluginAccessException(e.toString());
++ }
++ }
++
++ @NotNull
++ private Class<? extends Event> getRegistrationClass(@NotNull Class<? extends Event> clazz) {
++ try {
++ clazz.getDeclaredMethod("getHandlerList");
++ return clazz;
++ } catch (NoSuchMethodException e) {
++ if (clazz.getSuperclass() != null
++ && !clazz.getSuperclass().equals(Event.class)
++ && Event.class.isAssignableFrom(clazz.getSuperclass())) {
++ return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
++ } else {
++ throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!");
++ }
++ }
++ }
++
++ // JavaPluginLoader
++ @NotNull
++ public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
++ Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<>();
++
++ Set<Method> methods;
++ try {
++ Class<?> listenerClazz = listener.getClass();
++ methods = Sets.union(
++ Set.of(listenerClazz.getMethods()),
++ Set.of(listenerClazz.getDeclaredMethods())
++ );
++ } catch (NoClassDefFoundError e) {
++ plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
++ return ret;
++ }
++
++ for (final Method method : methods) {
++ final EventHandler eh = method.getAnnotation(EventHandler.class);
++ if (eh == null) continue;
++ // Do not register bridge or synthetic methods to avoid event duplication
++ // Fixes SPIGOT-893
++ if (method.isBridge() || method.isSynthetic()) {
++ continue;
++ }
++ final Class<?> checkClass;
++ if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
++ plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
++ continue;
++ }
++ final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
++ method.setAccessible(true);
++ Set<RegisteredListener> eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>());
++
++ for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
++ // This loop checks for extending deprecated events
++ if (clazz.getAnnotation(Deprecated.class) != null) {
++ Warning warning = clazz.getAnnotation(Warning.class);
++ Warning.WarningState warningState = this.server.getWarningState();
++ if (!warningState.printFor(warning)) {
++ break;
++ }
++ plugin.getLogger().log(
++ Level.WARNING,
++ String.format(
++ "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
++ plugin.getPluginMeta().getDisplayName(),
++ clazz.getName(),
++ method.toGenericString(),
++ (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
++ Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())),
++ warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null);
++ break;
++ }
++ }
++
++ EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass);
++ eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
++ }
++ return ret;
++ }
++
++ public void clearEvents() {
++ HandlerList.unregisterAll();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..afe793c35f05a80058e80bcaee76ac45a40b04a2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
+@@ -0,0 +1,201 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.collect.ImmutableSet;
++import io.papermc.paper.plugin.PermissionManager;
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.HashSet;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Set;
++import java.util.WeakHashMap;
++
++/**
++ * See
++ * {@link StupidSPMPermissionManagerWrapper}
++ */
++abstract class PaperPermissionManager implements PermissionManager {
++
++ public abstract Map<String, Permission> permissions();
++
++ public abstract Map<Boolean, Set<Permission>> defaultPerms();
++
++ public abstract Map<String, Map<Permissible, Boolean>> permSubs();
++
++ public abstract Map<Boolean, Map<Permissible, Boolean>> defSubs();
++
++ @Override
++ @Nullable
++ public Permission getPermission(@NotNull String name) {
++ return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH));
++ }
++
++ @Override
++ public void addPermission(@NotNull Permission perm) {
++ this.addPermission(perm, true);
++ }
++
++ @Override
++ public void addPermissions(@NotNull List<Permission> permissions) {
++ for (Permission permission : permissions) {
++ this.addPermission(permission, false);
++ }
++ this.dirtyPermissibles();
++ }
++
++ // Allow suppressing permission default calculations
++ private void addPermission(@NotNull Permission perm, boolean dirty) {
++ String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
++
++ if (this.permissions().containsKey(name)) {
++ throw new IllegalArgumentException("The permission " + name + " is already defined!");
++ }
++
++ this.permissions().put(name, perm);
++ this.calculatePermissionDefault(perm, dirty);
++ }
++
++ @Override
++ @NotNull
++ public Set<Permission> getDefaultPermissions(boolean op) {
++ return ImmutableSet.copyOf(this.defaultPerms().get(op));
++ }
++
++
++ @Override
++ public void removePermission(@NotNull Permission perm) {
++ this.removePermission(perm.getName());
++ }
++
++
++ @Override
++ public void removePermission(@NotNull String name) {
++ this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH));
++ }
++
++ @Override
++ public void recalculatePermissionDefaults(@NotNull Permission perm) {
++ // we need a null check here because some plugins for some unknown reason pass null into this?
++ if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ROOT))) {
++ this.defaultPerms().get(true).remove(perm);
++ this.defaultPerms().get(false).remove(perm);
++
++ this.calculatePermissionDefault(perm, true);
++ }
++ }
++
++ private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) {
++ if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
++ this.defaultPerms().get(true).add(perm);
++ if (dirty) {
++ this.dirtyPermissibles(true);
++ }
++ }
++ if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
++ this.defaultPerms().get(false).add(perm);
++ if (dirty) {
++ this.dirtyPermissibles(false);
++ }
++ }
++ }
++
++
++ @Override
++ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
++ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++ Map<Permissible, Boolean> map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>());
++
++ map.put(permissible, true);
++ }
++
++ @Override
++ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
++ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++ Map<Permissible, Boolean> map = this.permSubs().get(name);
++
++ if (map != null) {
++ map.remove(permissible);
++
++ if (map.isEmpty()) {
++ this.permSubs().remove(name);
++ }
++ }
++ }
++
++ @Override
++ @NotNull
++ public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
++ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++ Map<Permissible, Boolean> map = this.permSubs().get(name);
++
++ if (map == null) {
++ return ImmutableSet.of();
++ } else {
++ return ImmutableSet.copyOf(map.keySet());
++ }
++ }
++
++ @Override
++ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
++ Map<Permissible, Boolean> map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>());
++
++ map.put(permissible, true);
++ }
++
++ @Override
++ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
++ Map<Permissible, Boolean> map = this.defSubs().get(op);
++
++ if (map != null) {
++ map.remove(permissible);
++
++ if (map.isEmpty()) {
++ this.defSubs().remove(op);
++ }
++ }
++ }
++
++ @Override
++ @NotNull
++ public Set<Permissible> getDefaultPermSubscriptions(boolean op) {
++ Map<Permissible, Boolean> map = this.defSubs().get(op);
++
++ if (map == null) {
++ return ImmutableSet.of();
++ } else {
++ return ImmutableSet.copyOf(map.keySet());
++ }
++ }
++
++ @Override
++ @NotNull
++ public Set<Permission> getPermissions() {
++ return new HashSet<>(this.permissions().values());
++ }
++
++ @Override
++ public void clearPermissions() {
++ this.permissions().clear();
++ this.defaultPerms().get(true).clear();
++ this.defaultPerms().get(false).clear();
++ }
++
++
++ void dirtyPermissibles(boolean op) {
++ Set<Permissible> permissibles = this.getDefaultPermSubscriptions(op);
++
++ for (Permissible p : permissibles) {
++ p.recalculatePermissions();
++ }
++ }
++
++ void dirtyPermissibles() {
++ this.dirtyPermissibles(true);
++ this.dirtyPermissibles(false);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d2dee700f2c5cc7d6a272e751a933901fe7a55b6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
+@@ -0,0 +1,317 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.base.Preconditions;
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.source.DirectoryProviderSource;
++import io.papermc.paper.plugin.provider.source.FileArrayProviderSource;
++import io.papermc.paper.plugin.provider.source.FileProviderSource;
++import java.io.File;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.World;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandMap;
++import org.bukkit.command.PluginCommandYamlParser;
++import org.bukkit.craftbukkit.util.CraftMagicNumbers;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.server.PluginDisableEvent;
++import org.bukkit.event.server.PluginEnableEvent;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.IOException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.logging.Level;
++
++@SuppressWarnings("UnstableApiUsage")
++class PaperPluginInstanceManager {
++
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
++
++ private final List<Plugin> plugins = new ArrayList<>();
++ private final Map<String, Plugin> lookupNames = new HashMap<>();
++
++ private final PluginManager pluginManager;
++ private final CommandMap commandMap;
++ private final Server server;
++
++ private final MetaDependencyTree dependencyTree = new SimpleMetaDependencyTree(GraphBuilder.directed().build());
++
++ public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
++ this.commandMap = commandMap;
++ this.server = server;
++ this.pluginManager = pluginManager;
++ }
++
++ public @Nullable Plugin getPlugin(@NotNull String name) {
++ return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
++ }
++
++ public @NotNull Plugin[] getPlugins() {
++ return this.plugins.toArray(new Plugin[0]);
++ }
++
++ public boolean isPluginEnabled(@NotNull String name) {
++ Plugin plugin = this.getPlugin(name);
++
++ return this.isPluginEnabled(plugin);
++ }
++
++ public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
++ if ((plugin != null) && (this.plugins.contains(plugin))) {
++ return plugin.isEnabled();
++ } else {
++ return false;
++ }
++ }
++
++ public void loadPlugin(Plugin provided) {
++ PluginMeta configuration = provided.getPluginMeta();
++
++ this.plugins.add(provided);
++ this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
++ for (String providedPlugin : configuration.getProvidedPlugins()) {
++ this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
++ }
++
++ this.dependencyTree.add(configuration);
++ }
++
++ // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
++ public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
++ RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage(this.dependencyTree));
++
++ try {
++ path = FILE_PROVIDER_SOURCE.prepareContext(path);
++ FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
++ } catch (IllegalArgumentException exception) {
++ return null; // Return null when the plugin file is not valid / plugin type is unknown
++ } catch (PluginGraphCycleException exception) {
++ throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
++ } catch (Exception e) {
++ throw new InvalidPluginException(e);
++ }
++
++ try {
++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
++ } catch (Throwable e) {
++ throw new InvalidPluginException(e);
++ }
++
++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
++ .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
++ }
++
++ public @NotNull Plugin[] loadPlugins(@NotNull File[] files) {
++ RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
++ try {
++ List<Path> paths = FileArrayProviderSource.INSTANCE.prepareContext(files);
++ DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, paths);
++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
++ } catch (Exception e) {
++ // This should never happen, any errors that occur in this provider should instead be logged.
++ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
++ }
++
++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
++ }
++
++ // The behavior of this is that all errors are logged instead of being thrown
++ public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
++ Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
++
++ RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
++ try {
++ List<Path> files = DirectoryProviderSource.INSTANCE.prepareContext(directory);
++ DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, files);
++ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
++ } catch (Exception e) {
++ // This should never happen, any errors that occur in this provider should instead be logged.
++ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
++ }
++
++ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
++ }
++
++ // Plugins are disabled in order like this inorder to "rougly" prevent
++ // their dependencies unloading first. But, eh.
++ public void disablePlugins() {
++ Plugin[] plugins = this.getPlugins();
++ for (int i = plugins.length - 1; i >= 0; i--) {
++ this.disablePlugin(plugins[i]);
++ }
++ }
++
++ public void clearPlugins() {
++ synchronized (this) {
++ this.disablePlugins();
++ this.plugins.clear();
++ this.lookupNames.clear();
++ }
++ }
++
++ public synchronized void enablePlugin(@NotNull Plugin plugin) {
++ if (plugin.isEnabled()) {
++ return;
++ }
++
++ if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
++ List<Command> bukkitCommands = PluginCommandYamlParser.parse(plugin);
++
++ if (!bukkitCommands.isEmpty()) {
++ this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
++ }
++ }
++
++ try {
++ String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
++ if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
++ enableMsg += "*";
++ }
++ plugin.getLogger().info(enableMsg);
++
++ JavaPlugin jPlugin = (JavaPlugin) plugin;
++
++ if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
++ if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
++ this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
++ }
++ } // Paper
++
++ try {
++ jPlugin.setEnabled(true);
++ } catch (Throwable ex) {
++ this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
++ // Paper start - Disable plugins that fail to load
++ this.server.getPluginManager().disablePlugin(jPlugin);
++ return;
++ // Paper end
++ }
++
++ // Perhaps abort here, rather than continue going, but as it stands,
++ // an abort is not possible the way it's currently written
++ this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while enabling "
++ + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
++ }
++
++ HandlerList.bakeAll();
++ }
++
++ public synchronized void disablePlugin(@NotNull Plugin plugin) {
++ if (!(plugin instanceof JavaPlugin javaPlugin)) {
++ throw new IllegalArgumentException("Only expects java plugins.");
++ }
++ if (!plugin.isEnabled()) {
++ return;
++ }
++
++ String pluginName = plugin.getPluginMeta().getDisplayName();
++
++ try {
++ plugin.getLogger().info("Disabling %s".formatted(pluginName));
++
++ this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
++ try {
++ javaPlugin.setEnabled(false);
++ } catch (Throwable ex) {
++ this.server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + pluginName, ex);
++ }
++
++ ClassLoader classLoader = plugin.getClass().getClassLoader();
++ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ try {
++ configuredPluginClassLoader.close();
++ } catch (IOException ex) {
++ this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
++ }
++ // Remove from the classloader pool inorder to prevent plugins from trying
++ // to access classes
++ PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
++ }
++
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while disabling "
++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ try {
++ this.server.getScheduler().cancelTasks(plugin);
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ try {
++ this.server.getServicesManager().unregisterAll(plugin);
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ try {
++ HandlerList.unregisterAll(plugin);
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ try {
++ this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
++ this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
++ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ try {
++ for (World world : this.server.getWorlds()) {
++ world.removePluginChunkTickets(plugin);
++ }
++ } catch (Throwable ex) {
++ this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++ }
++
++ }
++
++ // TODO: Implement event part in future patch (paper patch move up, this patch is lower)
++ private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
++ Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
++ this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
++ }
++
++ public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
++ return this.dependencyTree.isTransitiveDependency(plugin, depend);
++ }
++
++ public boolean hasDependency(String pluginIdentifier) {
++ return this.getPlugin(pluginIdentifier) != null;
++ }
++
++ // Debug only
++ @ApiStatus.Internal
++ public MutableGraph<String> getDependencyGraph() {
++ return this.dependencyTree.getGraph();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..097500a59336db1bbfffcd1aa4cff7a8586e46ec
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
+@@ -0,0 +1,246 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.PermissionManager;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.command.CommandMap;
++import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventPriority;
++import org.bukkit.event.Listener;
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.EventExecutor;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.plugin.SimplePluginManager;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.util.List;
++import java.util.Set;
++
++public class PaperPluginManagerImpl implements PluginManager, DependencyContext {
++
++ final PaperPluginInstanceManager instanceManager;
++ final PaperEventManager paperEventManager;
++ PermissionManager permissionManager;
++
++ public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) {
++ this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server);
++ this.paperEventManager = new PaperEventManager(server);
++
++ if (permissionManager == null) {
++ this.permissionManager = new NormalPaperPermissionManager();
++ } else {
++ this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed
++ }
++ }
++
++ // REMOVE THIS WHEN SimplePluginManager is removed.
++ // Just cast and use Bukkit.getServer().getPluginManager()
++ public static PaperPluginManagerImpl getInstance() {
++ return ((CraftServer) (Bukkit.getServer())).paperPluginManager;
++ }
++
++ // Plugin Manipulation
++
++ @Override
++ public @Nullable Plugin getPlugin(@NotNull String name) {
++ return this.instanceManager.getPlugin(name);
++ }
++
++ @Override
++ public @NotNull Plugin[] getPlugins() {
++ return this.instanceManager.getPlugins();
++ }
++
++ @Override
++ public boolean isPluginEnabled(@NotNull String name) {
++ return this.instanceManager.isPluginEnabled(name);
++ }
++
++ @Override
++ public boolean isPluginEnabled(@Nullable Plugin plugin) {
++ return this.instanceManager.isPluginEnabled(plugin);
++ }
++
++ public void loadPlugin(Plugin plugin) {
++ this.instanceManager.loadPlugin(plugin);
++ }
++
++ @Override
++ public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException {
++ return this.instanceManager.loadPlugin(file.toPath());
++ }
++
++ @Override
++ public @NotNull Plugin[] loadPlugins(@NotNull File directory) {
++ return this.instanceManager.loadPlugins(directory.toPath());
++ }
++
++ @Override
++ public @NotNull Plugin[] loadPlugins(final @NotNull File[] files) {
++ return this.instanceManager.loadPlugins(files);
++ }
++
++ @Override
++ public void disablePlugins() {
++ this.instanceManager.disablePlugins();
++ }
++
++ @Override
++ public synchronized void clearPlugins() {
++ this.instanceManager.clearPlugins();
++ this.permissionManager.clearPermissions();
++ this.paperEventManager.clearEvents();
++ }
++
++ @Override
++ public void enablePlugin(@NotNull Plugin plugin) {
++ this.instanceManager.enablePlugin(plugin);
++ }
++
++ @Override
++ public void disablePlugin(@NotNull Plugin plugin) {
++ this.instanceManager.disablePlugin(plugin);
++ }
++
++ @Override
++ public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) {
++ return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig);
++ }
++
++ @Override
++ public boolean hasDependency(String pluginIdentifier) {
++ return this.instanceManager.hasDependency(pluginIdentifier);
++ }
++
++ // Event manipulation
++
++ @Override
++ public void callEvent(@NotNull Event event) throws IllegalStateException {
++ this.paperEventManager.callEvent(event);
++ }
++
++ @Override
++ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
++ this.paperEventManager.registerEvents(listener, plugin);
++ }
++
++ @Override
++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
++ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin);
++ }
++
++ @Override
++ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
++ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled);
++ }
++
++ // Permission manipulation
++
++ @Override
++ public @Nullable Permission getPermission(@NotNull String name) {
++ return this.permissionManager.getPermission(name);
++ }
++
++ @Override
++ public void addPermission(@NotNull Permission perm) {
++ this.permissionManager.addPermission(perm);
++ }
++
++ @Override
++ public void removePermission(@NotNull Permission perm) {
++ this.permissionManager.removePermission(perm);
++ }
++
++ @Override
++ public void removePermission(@NotNull String name) {
++ this.permissionManager.removePermission(name);
++ }
++
++ @Override
++ public @NotNull Set<Permission> getDefaultPermissions(boolean op) {
++ return this.permissionManager.getDefaultPermissions(op);
++ }
++
++ @Override
++ public void recalculatePermissionDefaults(@NotNull Permission perm) {
++ this.permissionManager.recalculatePermissionDefaults(perm);
++ }
++
++ @Override
++ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
++ this.permissionManager.subscribeToPermission(permission, permissible);
++ }
++
++ @Override
++ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
++ this.permissionManager.unsubscribeFromPermission(permission, permissible);
++ }
++
++ @Override
++ public @NotNull Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
++ return this.permissionManager.getPermissionSubscriptions(permission);
++ }
++
++ @Override
++ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
++ this.permissionManager.subscribeToDefaultPerms(op, permissible);
++ }
++
++ @Override
++ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
++ this.permissionManager.unsubscribeFromDefaultPerms(op, permissible);
++ }
++
++ @Override
++ public @NotNull Set<Permissible> getDefaultPermSubscriptions(boolean op) {
++ return this.permissionManager.getDefaultPermSubscriptions(op);
++ }
++
++ @Override
++ public @NotNull Set<Permission> getPermissions() {
++ return this.permissionManager.getPermissions();
++ }
++
++ @Override
++ public void addPermissions(@NotNull List<Permission> perm) {
++ this.permissionManager.addPermissions(perm);
++ }
++
++ @Override
++ public void clearPermissions() {
++ this.permissionManager.clearPermissions();
++ }
++
++ @Override
++ public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) {
++ this.permissionManager = permissionManager;
++ }
++
++ // Etc
++
++ @Override
++ public boolean useTimings() {
++ return co.aikar.timings.Timings.isTimingsEnabled();
++ }
++
++ @Override
++ public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException {
++ throw new UnsupportedOperationException();
++ }
++
++ public MutableGraph<String> getInstanceManagerGraph() {
++ return instanceManager.getDependencyGraph();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5d50d1d312388e979c0e1cd53a6bf5977ca6e549
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
+@@ -0,0 +1,47 @@
++package io.papermc.paper.plugin.manager;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * Used for loading plugins during runtime, only supporting providers that are plugins.
++ * This is only used for the plugin manager, as it only allows plugins to be
++ * registered to a provider storage.
++ */
++class RuntimePluginEntrypointHandler<T extends ProviderStorage<JavaPlugin>> implements EntrypointHandler {
++
++ private final T providerStorage;
++
++ RuntimePluginEntrypointHandler(T providerStorage) {
++ this.providerStorage = providerStorage;
++ }
++
++ @Override
++ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
++ if (!entrypoint.equals(Entrypoint.PLUGIN)) {
++ SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint)));
++ // We have to throw an invalid plugin exception for legacy reasons
++ }
++
++ this.providerStorage.register((PluginProvider<JavaPlugin>) provider);
++ }
++
++ @Override
++ public void enter(Entrypoint<?> entrypoint) {
++ if (entrypoint != Entrypoint.PLUGIN) {
++ throw new IllegalArgumentException("Only plugin entrypoint supported");
++ }
++ this.providerStorage.enter();
++ }
++
++ @NotNull
++ public T getPluginProviderStorage() {
++ return this.providerStorage;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b0e723bcda9b1fc01e6aa5e53e57c09ea4f1a1c8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
+@@ -0,0 +1,79 @@
++package io.papermc.paper.plugin.manager;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Optional;
++
++/**
++ * Used for registering a single plugin provider.
++ * This has special behavior in that some errors are thrown instead of logged.
++ */
++class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
++
++ private final MetaDependencyTree dependencyTree;
++ private PluginProvider<JavaPlugin> lastProvider;
++ private JavaPlugin singleLoaded;
++
++ SingularRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
++ this.dependencyTree = dependencyTree;
++ }
++
++ @Override
++ public void register(PluginProvider<JavaPlugin> provider) {
++ super.register(provider);
++ if (this.lastProvider != null) {
++ SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins"));
++ }
++ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++ throw new IllegalStateException("Cannot register paper plugins during runtime!");
++ }
++ this.lastProvider = provider;
++ // Register the provider into the server entrypoint, this allows it to show in /plugins correctly.
++ // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense.
++ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
++ }
++
++ @Override
++ public void enter() {
++ PluginProvider<JavaPlugin> provider = this.lastProvider;
++ if (provider == null) {
++ return;
++ }
++
++ // Go through normal plugin loading logic
++ super.enter();
++ }
++
++ @Override
++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
++ super.processProvided(provider, provided);
++ this.singleLoaded = provided;
++ }
++
++ @Override
++ public boolean throwOnCycle() {
++ return false;
++ }
++
++ public Optional<JavaPlugin> getSingleLoaded() {
++ return Optional.ofNullable(this.singleLoaded);
++ }
++
++ @Override
++ public MetaDependencyTree createDependencyTree() {
++ return this.dependencyTree;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ea8cf22c35242eb9f3914b95df00e20504aef5c1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
+@@ -0,0 +1,42 @@
++package io.papermc.paper.plugin.manager;
++
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.SimplePluginManager;
++
++import java.util.Map;
++import java.util.Set;
++
++/*
++This is actually so cursed I hate it.
++We need to wrap these in fields as people override the fields, so we need to access them lazily at all times.
++// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields.
++ */
++class StupidSPMPermissionManagerWrapper extends PaperPermissionManager {
++
++ private final SimplePluginManager simplePluginManager;
++
++ public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) {
++ this.simplePluginManager = simplePluginManager;
++ }
++
++ @Override
++ public Map<String, Permission> permissions() {
++ return this.simplePluginManager.permissions;
++ }
++
++ @Override
++ public Map<Boolean, Set<Permission>> defaultPerms() {
++ return this.simplePluginManager.defaultPerms;
++ }
++
++ @Override
++ public Map<String, Map<Permissible, Boolean>> permSubs() {
++ return this.simplePluginManager.permSubs;
++ }
++
++ @Override
++ public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
++ return this.simplePluginManager.defSubs;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fd9748c693a029c0d75f6ad813ea46a2b528d140
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
+@@ -0,0 +1,56 @@
++package io.papermc.paper.plugin.provider;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.List;
++import java.util.Map;
++import java.util.jar.JarFile;
++
++/**
++ * PluginProviders are created by a {@link io.papermc.paper.plugin.provider.source.ProviderSource},
++ * which is loaded into an {@link io.papermc.paper.plugin.entrypoint.EntrypointHandler}.
++ * <p>
++ * A PluginProvider is responsible for providing part of a plugin, whether it's a Bootstrapper or Server Plugin.
++ * The point of this class is to be able to create the actual instance later, as at the time this is created the server
++ * may be missing some key parts. For example, the Bukkit singleton will not be initialized yet, therefor we need to
++ * have a PluginServerProvider load the server plugin later.
++ * <p>
++ * Plugin providers are currently not exposed in any way of the api. It is preferred that this stays this way,
++ * as providers are only needed for initialization.
++ *
++ * @param <T> provider type
++ */
++public interface PluginProvider<T> {
++
++ @NotNull
++ Path getSource();
++
++ default Path getFileName() {
++ return this.getSource().getFileName();
++ }
++
++ default Path getParentSource() {
++ return this.getSource().getParent();
++ }
++
++ JarFile file();
++
++ T createInstance();
++
++ PluginMeta getMeta();
++
++ ComponentLogger getLogger();
++
++ LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad);
++
++ // Returns a list of missing dependencies
++ List<String> validateDependencies(@NotNull DependencyContext context);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6154e864b0ff01cb70acaaeee5ca8c9f4a90a90e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
+@@ -0,0 +1,13 @@
++package io.papermc.paper.plugin.provider;
++
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * This is used for the /plugins command, where it will look in the {@link io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler} and
++ * use the provider statuses to determine the color.
++ */
++public enum ProviderStatus {
++ INITIALIZED,
++ ERRORED,
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..17284d0f61c459dff765c0adae4ad2c641e054c1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
+@@ -0,0 +1,11 @@
++package io.papermc.paper.plugin.provider;
++
++/**
++ * This is used to mark that a plugin provider is able to hold a status for the /plugins command.
++ */
++public interface ProviderStatusHolder {
++
++ ProviderStatus getLastProvidedStatus();
++
++ void setStatus(ProviderStatus status);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6ba3bcc468c0a60c76d6d0f0243bda661c737f2f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
+@@ -0,0 +1,29 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.AnnotatedElement;
++
++@Retention(RetentionPolicy.RUNTIME)
++@Target(ElementType.FIELD)
++public @interface FlattenedResolver {
++
++ final class Factory implements NodeResolver.Factory {
++
++ @Override
++ public @Nullable NodeResolver make(String name, AnnotatedElement element) {
++ if (element.isAnnotationPresent(FlattenedResolver.class)) {
++ return (node) -> node;
++ } else {
++ return null;
++ }
++ }
++ }
++
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8cd649c977172f6b757d68565fcbb9eb8ae100a3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java
+@@ -0,0 +1,147 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import com.google.gson.reflect.TypeToken;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle;
++import org.spongepowered.configurate.CommentedConfigurationNode;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.NodePath;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.transformation.ConfigurationTransformation;
++
++import java.util.EnumMap;
++import java.util.EnumSet;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++
++class LegacyPaperMeta {
++
++
++ private static final TypeToken<Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>>> TYPE_TOKEN = new TypeToken<>() {
++ };
++
++ public static void migrate(CommentedConfigurationNode node) throws ConfigurateException {
++ ConfigurationTransformation.chain(notVersioned()).apply(node);
++ }
++
++ private static ConfigurationTransformation notVersioned() {
++ return ConfigurationTransformation.builder()
++ .addAction(NodePath.path(), (path, value) -> {
++ boolean bootstrapSubSection = value.hasChild("bootstrap");
++ boolean serverSubSection = value.hasChild("server");
++
++ // Ignore if using newer format
++ if (bootstrapSubSection || serverSubSection) {
++ return null;
++ }
++
++ // First collect all load before elements
++ LegacyConfiguration legacyConfiguration;
++ try {
++ legacyConfiguration = value.require(LegacyConfiguration.class);
++ } catch (SerializationException exception) {
++ // Ignore if not present
++ return null;
++ }
++
++ Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class);
++ dependencies.put(PluginDependencyLifeCycle.BOOTSTRAP, new HashMap<>());
++ dependencies.put(PluginDependencyLifeCycle.SERVER, new HashMap<>());
++
++ Map<PluginDependencyLifeCycle, Map<String, Set<DependencyFlag>>> dependencyConfigurationMap = new HashMap<>();
++ dependencyConfigurationMap.put(PluginDependencyLifeCycle.BOOTSTRAP, new HashMap<>());
++ dependencyConfigurationMap.put(PluginDependencyLifeCycle.SERVER, new HashMap<>());
++
++ // Migrate loadafter
++ for (LegacyLoadConfiguration legacyConfig : legacyConfiguration.loadAfter) {
++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap
++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER)
++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class));
++
++ dependencyFlags.add(DependencyFlag.LOAD_AFTER);
++ }
++
++ // Migrate loadbefore
++ for (LegacyLoadConfiguration legacyConfig : legacyConfiguration.loadBefore) {
++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap
++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER)
++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class));
++
++ dependencyFlags.add(DependencyFlag.LOAD_BEFORE);
++ }
++
++ // Migrate dependencies
++ for (LegacyDependencyConfiguration legacyConfig : legacyConfiguration.dependencies) {
++ Set<DependencyFlag> dependencyFlags = dependencyConfigurationMap
++ .get(legacyConfig.bootstrap ? PluginDependencyLifeCycle.BOOTSTRAP : PluginDependencyLifeCycle.SERVER)
++ .computeIfAbsent(legacyConfig.name, s -> EnumSet.noneOf(DependencyFlag.class));
++
++ dependencyFlags.add(DependencyFlag.DEPENDENCY);
++ if (legacyConfig.required) {
++ dependencyFlags.add(DependencyFlag.REQUIRED);
++ }
++ }
++ for (Map.Entry<PluginDependencyLifeCycle, Map<String, Set<DependencyFlag>>> legacyTypes : dependencyConfigurationMap.entrySet()) {
++ Map<String, DependencyConfiguration> flagMap = dependencies.get(legacyTypes.getKey());
++ for (Map.Entry<String, Set<DependencyFlag>> entry : legacyTypes.getValue().entrySet()) {
++ Set<DependencyFlag> flags = entry.getValue();
++
++
++ DependencyConfiguration.LoadOrder loadOrder = DependencyConfiguration.LoadOrder.OMIT;
++ // These meanings are now swapped
++ if (flags.contains(DependencyFlag.LOAD_BEFORE)) {
++ loadOrder = DependencyConfiguration.LoadOrder.AFTER;
++ } else if (flags.contains(DependencyFlag.LOAD_AFTER)) {
++ loadOrder = DependencyConfiguration.LoadOrder.BEFORE;
++ }
++
++ flagMap.put(entry.getKey(), new DependencyConfiguration(
++ loadOrder,
++ flags.contains(DependencyFlag.REQUIRED),
++ flags.contains(DependencyFlag.DEPENDENCY)
++ ));
++ }
++ }
++
++ value.node("dependencies").set(TYPE_TOKEN.getType(), dependencies);
++ return null;
++ })
++ .build();
++ }
++
++ @ConfigSerializable
++ record LegacyLoadConfiguration(
++ @Required String name,
++ boolean bootstrap
++ ) {
++ }
++
++ @ConfigSerializable
++ private static class LegacyConfiguration {
++
++ private List<LegacyLoadConfiguration> loadAfter = List.of();
++ private List<LegacyLoadConfiguration> loadBefore = List.of();
++ private List<LegacyDependencyConfiguration> dependencies = List.of();
++ }
++
++
++ @ConfigSerializable
++ public record LegacyDependencyConfiguration(
++ @Required String name,
++ boolean required,
++ boolean bootstrap
++ ) {
++ }
++
++ enum DependencyFlag {
++ LOAD_AFTER,
++ LOAD_BEFORE,
++ REQUIRED,
++ DEPENDENCY
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e3430f535e8e9c3b8b44bf2daece8c47e8b14db7
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/LoadOrderConfiguration.java
+@@ -0,0 +1,38 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.List;
++
++/**
++ * This is used for plugins to configure the load order of strategies.
++ */
++public interface LoadOrderConfiguration {
++
++ /**
++ * Provides a list of plugins that THIS configuration should load
++ * before.
++ *
++ * @return list of plugins
++ */
++ @NotNull
++ List<String> getLoadBefore();
++
++ /**
++ * Provides a list of plugins that THIS configuration should load
++ * before.
++ *
++ * @return list of plugins
++ */
++ @NotNull
++ List<String> getLoadAfter();
++
++ /**
++ * Gets the responsible plugin provider's meta.
++ *
++ * @return meta
++ */
++ @NotNull
++ PluginMeta getMeta();
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d3b3a8baca013909fa9c6204d964d7d7efeb2719
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
+@@ -0,0 +1,271 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import com.google.common.base.Preconditions;
++import com.google.common.collect.ImmutableList;
++import io.papermc.paper.configuration.constraint.Constraint;
++import io.papermc.paper.configuration.serializer.ComponentSerializer;
++import io.papermc.paper.configuration.serializer.EnumValueSerializer;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
++import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle;
++import java.lang.reflect.Type;
++import java.util.function.Predicate;
++import org.bukkit.craftbukkit.util.ApiVersion;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginLoadOrder;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.jetbrains.annotations.TestOnly;
++import org.spongepowered.configurate.CommentedConfigurationNode;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.loader.HeaderMode;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.ObjectMapper;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++import org.spongepowered.configurate.serialize.ScalarSerializer;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.yaml.NodeStyle;
++import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
++
++import java.io.BufferedReader;
++import java.util.EnumMap;
++import java.util.List;
++import java.util.Map;
++
++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
++@ConfigSerializable
++public class PaperPluginMeta implements PluginMeta {
++
++ @PluginConfigConstraints.PluginName
++ @Required
++ private String name;
++ @Required
++ @PluginConfigConstraints.PluginNameSpace
++ private String main;
++ @PluginConfigConstraints.PluginNameSpace
++ private String bootstrapper;
++ @PluginConfigConstraints.PluginNameSpace
++ private String loader;
++ private List<String> provides = List.of();
++ private boolean hasOpenClassloader = false;
++ @Required
++ private String version;
++ private String description;
++ private List<String> authors = List.of();
++ private List<String> contributors = List.of();
++ private String website;
++ private String prefix;
++ private PluginLoadOrder load = PluginLoadOrder.POSTWORLD;
++ @FlattenedResolver
++ private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of());
++ @Required
++ private ApiVersion apiVersion;
++
++ private Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class);
++
++ public PaperPluginMeta() {
++ }
++
++ static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.19");
++ public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException {
++ YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
++ .indent(2)
++ .nodeStyle(NodeStyle.BLOCK)
++ .headerMode(HeaderMode.NONE)
++ .source(() -> reader)
++ .defaultOptions((options) -> {
++
++ return options.serializers((serializers) -> {
++ serializers
++ .register(new ScalarSerializer<>(ApiVersion.class) {
++ @Override
++ public ApiVersion deserialize(final Type type, final Object obj) throws SerializationException {
++ try {
++ final ApiVersion version = ApiVersion.getOrCreateVersion(obj.toString());
++ if (version.isOlderThan(MINIMUM)) {
++ throw new SerializationException(version + " is too old for a paper plugin!");
++ }
++ return version;
++ } catch (final IllegalArgumentException e) {
++ throw new SerializationException(e);
++ }
++ }
++
++ @Override
++ protected Object serialize(final ApiVersion item, final Predicate<Class<?>> typeSupported) {
++ return item.getVersionString();
++ }
++ })
++ .register(new EnumValueSerializer())
++ .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER)
++ .register(new ComponentSerializer())
++ .registerAnnotatedObjects(
++ ObjectMapper.factoryBuilder()
++ .addConstraint(Constraint.class, new Constraint.Factory())
++ .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory())
++ .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory())
++ .addNodeResolver(new FlattenedResolver.Factory())
++ .build()
++ );
++
++ });
++ })
++ .build();
++ CommentedConfigurationNode node = loader.load();
++ LegacyPaperMeta.migrate(node);
++ PaperPluginMeta pluginConfiguration = node.require(PaperPluginMeta.class);
++
++ if (!node.node("author").virtual()) {
++ pluginConfiguration.authors = ImmutableList.<String>builder()
++ .addAll(pluginConfiguration.authors)
++ .add(node.node("author").getString())
++ .build();
++ }
++
++ return pluginConfiguration;
++ }
++
++ @Override
++ public @NotNull String getName() {
++ return this.name;
++ }
++
++ @TestOnly
++ public void setName(@NotNull String name) {
++ Preconditions.checkNotNull(name, "name");
++ this.name = name;
++ }
++
++ @Override
++ public @NotNull String getMainClass() {
++ return this.main;
++ }
++
++ @Override
++ public @NotNull String getVersion() {
++ return this.version;
++ }
++
++ @TestOnly
++ public void setVersion(@NotNull String version) {
++ Preconditions.checkNotNull(version, "version");
++ this.version = version;
++ }
++
++ @Override
++ public @Nullable String getLoggerPrefix() {
++ return this.prefix;
++ }
++
++ @Override
++ public @NotNull List<String> getPluginDependencies() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of())
++ .entrySet()
++ .stream()
++ .filter((entry) -> entry.getValue().required() && entry.getValue().joinClasspath())
++ .map(Map.Entry::getKey)
++ .toList();
++ }
++
++ @Override
++ public @NotNull List<String> getPluginSoftDependencies() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of())
++ .entrySet()
++ .stream()
++ .filter((entry) -> !entry.getValue().required() && entry.getValue().joinClasspath())
++ .map(Map.Entry::getKey)
++ .toList();
++ }
++
++ @Override
++ public @NotNull List<String> getLoadBeforePlugins() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of())
++ .entrySet()
++ .stream()
++ // This plugin will load BEFORE all dependencies (so dependencies will load AFTER plugin)
++ .filter((entry) -> entry.getValue().load() == DependencyConfiguration.LoadOrder.AFTER)
++ .map(Map.Entry::getKey)
++ .toList();
++ }
++
++ public @NotNull List<String> getLoadAfterPlugins() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of())
++ .entrySet()
++ .stream()
++ // This plugin will load AFTER all dependencies (so dependencies will load BEFORE plugin)
++ .filter((entry) -> entry.getValue().load() == DependencyConfiguration.LoadOrder.BEFORE)
++ .map(Map.Entry::getKey)
++ .toList();
++ }
++
++
++ public Map<String, DependencyConfiguration> getServerDependencies() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.SERVER, Map.of());
++ }
++
++ public Map<String, DependencyConfiguration> getBootstrapDependencies() {
++ return this.dependencies.getOrDefault(PluginDependencyLifeCycle.BOOTSTRAP, Map.of());
++ }
++
++ @Override
++ public @NotNull PluginLoadOrder getLoadOrder() {
++ return this.load;
++ }
++
++ @Override
++ public @NotNull String getDescription() {
++ return this.description;
++ }
++
++ @Override
++ public @NotNull List<String> getAuthors() {
++ return this.authors;
++ }
++
++ @Override
++ public @NotNull List<String> getContributors() {
++ return this.contributors;
++ }
++
++ @Override
++ public String getWebsite() {
++ return this.website;
++ }
++
++ @Override
++ public @NotNull List<Permission> getPermissions() {
++ return this.permissionConfiguration.permissions();
++ }
++
++ @Override
++ public @NotNull PermissionDefault getPermissionDefault() {
++ return this.permissionConfiguration.defaultPerm();
++ }
++
++ @Override
++ public @NotNull String getAPIVersion() {
++ return this.apiVersion.getVersionString();
++ }
++
++ @Override
++ public @NotNull List<String> getProvidedPlugins() {
++ return this.provides;
++ }
++
++ public String getBootstrapper() {
++ return this.bootstrapper;
++ }
++
++ public String getLoader() {
++ return this.loader;
++ }
++
++ public boolean hasOpenClassloader() {
++ return this.hasOpenClassloader;
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f0cdb1bab30faaa438aa3e6de6125ade3fae98c2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
+@@ -0,0 +1,90 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import com.google.common.collect.ImmutableCollection;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++import org.spongepowered.configurate.util.CheckedConsumer;
++
++import java.lang.reflect.Type;
++import java.util.Collection;
++import java.util.Collections;
++import java.util.List;
++
++@SuppressWarnings("unchecked")
++public abstract class ImmutableCollectionSerializer<B extends ImmutableCollection.Builder<?>, T extends Collection<?>> implements TypeSerializer<T> {
++
++ protected ImmutableCollectionSerializer() {
++ }
++
++ @Override
++ public final T deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
++ final Type entryType = this.elementType(type);
++ final @Nullable TypeSerializer<?> entrySerial = node.options().serializers().get(entryType);
++ if (entrySerial == null) {
++ throw new SerializationException(node, entryType, "No applicable type serializer for type");
++ }
++
++ if (node.isList()) {
++ final List<? extends ConfigurationNode> values = node.childrenList();
++ final B builder = this.createNew(values.size());
++ for (ConfigurationNode value : values) {
++ try {
++ this.deserializeSingle(builder, entrySerial.deserialize(entryType, value));
++ } catch (final SerializationException ex) {
++ ex.initPath(value::path);
++ throw ex;
++ }
++ }
++ return (T) builder.build();
++ } else {
++ final @Nullable Object unwrappedVal = node.raw();
++ if (unwrappedVal != null) {
++ final B builder = this.createNew(1);
++ this.deserializeSingle(builder, entrySerial.deserialize(entryType, node));
++ return (T) builder.build();
++ }
++ }
++ return this.emptyValue(type, null);
++ }
++
++ @SuppressWarnings({"unchecked", "rawtypes"})
++ @Override
++ public final void serialize(final Type type, final @Nullable T obj, final ConfigurationNode node) throws SerializationException {
++ final Type entryType = this.elementType(type);
++ final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType);
++ if (entrySerial == null) {
++ throw new SerializationException(node, entryType, "No applicable type serializer for type");
++ }
++
++ node.raw(Collections.emptyList());
++ if (obj != null) {
++ this.forEachElement(obj, el -> {
++ final ConfigurationNode child = node.appendListNode();
++ try {
++ entrySerial.serialize(entryType, el, child);
++ } catch (final SerializationException ex) {
++ ex.initPath(child::path);
++ throw ex;
++ }
++ });
++ }
++ }
++
++ @SuppressWarnings({"unchecked"})
++ @Override
++ public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) {
++ return (T) this.createNew(0).build();
++ }
++
++ protected abstract Type elementType(Type containerType) throws SerializationException;
++
++ protected abstract B createNew(int size);
++
++ protected abstract void forEachElement(T collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException;
++
++ protected abstract void deserializeSingle(B builder, @Nullable Object deserialized) throws SerializationException;
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7757d7df70e39a6fe4d92d02b6f905a22f80dbf3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
+@@ -0,0 +1,43 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import com.google.common.collect.ImmutableList;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.util.CheckedConsumer;
++
++import java.lang.reflect.ParameterizedType;
++import java.lang.reflect.Type;
++import java.util.List;
++
++public class ImmutableListSerializer extends ImmutableCollectionSerializer<ImmutableList.Builder<?>, List<?>> {
++
++ @Override
++ protected Type elementType(Type containerType) throws SerializationException {
++ if (!(containerType instanceof ParameterizedType)) {
++ throw new SerializationException(containerType, "Raw types are not supported for collections");
++ }
++ return ((ParameterizedType) containerType).getActualTypeArguments()[0];
++ }
++
++ @Override
++ protected ImmutableList.Builder<?> createNew(int size) {
++ return ImmutableList.builderWithExpectedSize(size);
++ }
++
++ @Override
++ protected void forEachElement(List<?> collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException {
++ for (Object obj : collection) {
++ action.accept(obj);
++ }
++ }
++
++ @SuppressWarnings({"unchecked", "rawtypes"})
++ @Override
++ protected void deserializeSingle(ImmutableList.Builder<?> builder, @Nullable Object deserialized) throws SerializationException {
++ if (deserialized == null) {
++ return;
++ }
++
++ ((ImmutableList.Builder) builder).add(deserialized);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f951f4024745503e9cdfa7ff17b9313ac6d7b4c4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
+@@ -0,0 +1,56 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++
++import java.lang.reflect.Type;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++public class PermissionConfigurationSerializer {
++
++ public static final Serializer SERIALIZER = new Serializer();
++
++ private static final class Serializer implements TypeSerializer<PermissionConfiguration> {
++ private Serializer() {
++ super();
++ }
++
++ @Override
++ public PermissionConfiguration deserialize(Type type, ConfigurationNode node) throws SerializationException {
++ Map<?, ?> map = (Map<?, ?>) node.node("permissions").raw();
++
++ PermissionDefault permissionDefault;
++ ConfigurationNode permNode = node.node("defaultPerm");
++ if (permNode.virtual()) {
++ permissionDefault = PermissionDefault.OP;
++ } else {
++ permissionDefault = PermissionDefault.getByName(permNode.getString());
++ }
++
++ List<Permission> result = new ArrayList<>();
++ if (map != null) {
++ for (Map.Entry<?, ?> entry : map.entrySet()) {
++ try {
++ result.add(Permission.loadPermission(entry.getKey().toString(), (Map<?, ?>) entry.getValue(), permissionDefault, result));
++ } catch (Throwable ex) {
++ throw new SerializationException((Type) null, "Error loading permission %s".formatted(entry.getKey()), ex);
++ }
++ }
++ }
++
++ return new PermissionConfiguration(permissionDefault, List.copyOf(result));
++ }
++
++ @Override
++ public void serialize(Type type, @org.checkerframework.checker.nullness.qual.Nullable PermissionConfiguration obj, ConfigurationNode node) throws SerializationException {
++
++ }
++
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3043a4216ec13c3de0cb931f11492ded1e6dc8de
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
+@@ -0,0 +1,67 @@
++package io.papermc.paper.plugin.provider.configuration.serializer.constraints;
++
++import io.papermc.paper.plugin.util.NamespaceChecker;
++import org.spongepowered.configurate.objectmapping.meta.Constraint;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.Type;
++import java.util.Locale;
++import java.util.Set;
++import java.util.regex.Pattern;
++
++public final class PluginConfigConstraints {
++
++ public static final Set<String> RESERVED_KEYS = Set.of("bukkit", "minecraft", "mojang", "spigot", "paper");
++
++ @Documented
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target(ElementType.FIELD)
++ public @interface PluginName {
++
++ final class Factory implements Constraint.Factory<PluginName, String> {
++
++ private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z\\d _.-]+$");
++
++ @Override
++ public Constraint<String> make(PluginName data, Type type) {
++ return value -> {
++ if (value != null) {
++ if (RESERVED_KEYS.contains(value.toLowerCase(Locale.ROOT))) {
++ throw new SerializationException("Restricted name, cannot use '%s' as a plugin name.".formatted(data));
++ } else if (value.indexOf(' ') != -1) {
++ // For legacy reasons, the space condition has a separate exception message.
++ throw new SerializationException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
++ }
++
++ if (!VALID_NAME.matcher(value).matches()) {
++ throw new SerializationException("name '" + value + "' contains invalid characters.");
++ }
++ }
++ };
++ }
++ }
++ }
++
++ @Documented
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target(ElementType.FIELD)
++ public @interface PluginNameSpace {
++
++ final class Factory implements Constraint.Factory<PluginNameSpace, String> {
++
++ @Override
++ public Constraint<String> make(PluginNameSpace data, Type type) {
++ return value -> {
++ if (value != null && !NamespaceChecker.isValidNameSpace(value)) {
++ throw new SerializationException("provided class '%s' is in an invalid namespace.".formatted(value));
++ }
++ };
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..957fb5e60ba6bd8760e8f6016d7bb6e8a405e163
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
+@@ -0,0 +1,30 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++
++import static java.util.Objects.requireNonNullElse;
++
++@DefaultQualifier(NonNull.class)
++@ConfigSerializable
++public record DependencyConfiguration(
++ LoadOrder load,
++ Boolean required,
++ Boolean joinClasspath
++) {
++ @SuppressWarnings("DataFlowIssue") // incorrect intellij inspections
++ public DependencyConfiguration {
++ required = requireNonNullElse(required, true);
++ joinClasspath = requireNonNullElse(joinClasspath, true);
++ }
++
++ @ConfigSerializable
++ public enum LoadOrder {
++ // dependency will now load BEFORE your plugin
++ BEFORE,
++ // the dependency will now load AFTER your plugin
++ AFTER,
++ OMIT
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4184e4232c59f15ef8bbc98f82f501fc524f37c7
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/LoadConfiguration.java
+@@ -0,0 +1,11 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++
++@ConfigSerializable
++public record LoadConfiguration(
++ @Required String name,
++ boolean bootstrap
++) {
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a180612a1ec395202dbae1ca5b97ec01382097e4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
+@@ -0,0 +1,14 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++
++import java.util.List;
++
++// Record components used for deserialization!!!!
++@ConfigSerializable
++public record PermissionConfiguration(
++ PermissionDefault defaultPerm,
++ List<Permission> permissions) {
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..49a087381307eab263f7dad43aaa25980db33cc2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PluginDependencyLifeCycle.java
+@@ -0,0 +1,6 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++public enum PluginDependencyLifeCycle {
++ BOOTSTRAP,
++ SERVER
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..226f457db6c1461c943c157b2b91e7450abc9dc6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+@@ -0,0 +1,66 @@
++package io.papermc.paper.plugin.provider.source;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import java.io.IOException;
++import java.nio.file.FileVisitOption;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.function.Consumer;
++import org.slf4j.Logger;
++
++/**
++ * Loads all plugin providers in the given directory.
++ */
++public class DirectoryProviderSource implements ProviderSource<Path, List<Path>> {
++
++ public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource();
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted);
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ @Override
++ public List<Path> prepareContext(Path context) throws IOException {
++ // Symlink happy, create file if missing.
++ if (!Files.isDirectory(context)) {
++ Files.createDirectories(context);
++ }
++
++ final List<Path> files = new ArrayList<>();
++ this.walkFiles(context, path -> {
++ try {
++ files.add(FILE_PROVIDER_SOURCE.prepareContext(path));
++ } catch (IllegalArgumentException ignored) {
++ // Ignore illegal argument exceptions from jar checking
++ } catch (final Exception e) {
++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e);
++ }
++ });
++ return files;
++ }
++
++ @Override
++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) {
++ for (Path path : context) {
++ try {
++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path);
++ } catch (IllegalArgumentException ignored) {
++ // Ignore illegal argument exceptions from jar checking
++ } catch (Exception e) {
++ LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++ }
++ }
++ }
++
++ private void walkFiles(Path context, Consumer<Path> consumer) throws IOException {
++ Files.walk(context, 1, FileVisitOption.FOLLOW_LINKS)
++ .filter(this::isValidFile)
++ .forEach(consumer);
++ }
++
++ public boolean isValidFile(Path path) {
++ // Avoid loading plugins that start with a dot
++ return Files.isRegularFile(path) && !path.startsWith(".");
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b2ec31fdb2ba8f18c29f2014c03c96a15ec995ad
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileArrayProviderSource.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.plugin.provider.source;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import java.io.File;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import org.slf4j.Logger;
++
++public class FileArrayProviderSource implements ProviderSource<File[], List<Path>> {
++
++ public static final FileArrayProviderSource INSTANCE = new FileArrayProviderSource();
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ @Override
++ public List<Path> prepareContext(File[] context) {
++ final List<Path> files = new ArrayList<>();
++ for (File file : context) {
++ try {
++ files.add(FILE_PROVIDER_SOURCE.prepareContext(file.toPath()));
++ } catch (IllegalArgumentException ignored) {
++ // Ignore illegal argument exceptions from jar checking
++ } catch (final Exception e) {
++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e);
++ }
++ }
++ return files;
++ }
++
++ @Override
++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) {
++ for (Path path : context) {
++ try {
++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path);
++ } catch (IllegalArgumentException ignored) {
++ // Ignore illegal argument exceptions from jar checking
++ } catch (Exception e) {
++ LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5b58df8df7efca0f67e3a14dd71051dfd7a26079
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+@@ -0,0 +1,171 @@
++package io.papermc.paper.plugin.provider.source;
++
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.type.PluginFileType;
++import org.bukkit.plugin.InvalidPluginException;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.io.IOException;
++import java.nio.file.FileVisitResult;
++import java.nio.file.FileVisitor;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.StandardCopyOption;
++import java.nio.file.attribute.BasicFileAttributes;
++import java.util.Set;
++import java.util.function.Function;
++import java.util.jar.JarFile;
++
++/**
++ * Loads a plugin provider at the given plugin jar file path.
++ */
++public class FileProviderSource implements ProviderSource<Path, Path> {
++
++ private final Function<Path, String> contextChecker;
++
++ public FileProviderSource(Function<Path, String> contextChecker) {
++ this.contextChecker = contextChecker;
++ }
++
++ @Override
++ public Path prepareContext(Path context) throws IOException {
++ String source = this.contextChecker.apply(context);
++
++ if (Files.notExists(context)) {
++ throw new IllegalArgumentException(source + " does not exist, cannot load a plugin from it!");
++ }
++
++ if (!Files.isRegularFile(context)) {
++ throw new IllegalArgumentException(source + " is not a file, cannot load a plugin from it!");
++ }
++
++ if (!context.getFileName().toString().endsWith(".jar")) {
++ throw new IllegalArgumentException(source + " is not a jar file, cannot load a plugin from it!");
++ }
++
++ try {
++ context = this.checkUpdate(context);
++ } catch (Exception exception) {
++ throw new RuntimeException(source + " failed to update!", exception);
++ }
++ return context;
++ }
++
++ @Override
++ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception {
++ String source = this.contextChecker.apply(context);
++
++ JarFile file = new JarFile(context.toFile(), true, JarFile.OPEN_READ, JarFile.runtimeVersion());
++ PluginFileType<?, ?> type = PluginFileType.guessType(file);
++ if (type == null) {
++ // Throw IAE wrapped in RE to prevent callers from considering this a "invalid parameter" as caller ignores IAE.
++ // TODO: This needs some heavy rework, using illegal argument exception to signal an actual failure is less than ideal.
++ if (file.getEntry("META-INF/versions.list") != null) {
++ throw new RuntimeException(new IllegalArgumentException(context + " appears to be a server jar! Server jars do not belong in the plugin folder."));
++ }
++
++ throw new RuntimeException(
++ new IllegalArgumentException(source + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!")
++ );
++ }
++
++ type.register(entrypointHandler, file, context);
++ }
++
++ /**
++ * Replaces a plugin with a plugin of the same plugin name in the update folder.
++ *
++ * @param file
++ */
++ private Path checkUpdate(Path file) throws InvalidPluginException {
++ PluginInitializerManager pluginSystem = PluginInitializerManager.instance();
++ Path updateDirectory = pluginSystem.pluginUpdatePath();
++ if (updateDirectory == null || !Files.isDirectory(updateDirectory)) {
++ return file;
++ }
++
++ try {
++ String pluginName = this.getPluginName(file);
++ UpdateFileVisitor visitor = new UpdateFileVisitor(pluginName);
++ Files.walkFileTree(updateDirectory, Set.of(), 1, visitor);
++ if (visitor.getValidPlugin() != null) {
++ Path updateLocation = visitor.getValidPlugin();
++
++ try {
++ Files.copy(updateLocation, file, StandardCopyOption.REPLACE_EXISTING);
++ } catch (IOException exception) {
++ throw new RuntimeException("Could not copy '" + updateLocation + "' to '" + file + "' in update plugin process", exception);
++ }
++
++ // Idk what this is about, TODO
++ File newName = new File(file.toFile().getParentFile(), updateLocation.toFile().getName());
++ file.toFile().renameTo(newName);
++ updateLocation.toFile().delete();
++ return newName.toPath();
++ }
++ } catch (Exception e) {
++ throw new InvalidPluginException(e);
++ }
++ return file;
++ }
++
++ private String getPluginName(Path path) throws Exception {
++ try (JarFile file = new JarFile(path.toFile())) {
++ PluginFileType<?, ?> type = PluginFileType.guessType(file);
++ if (type == null) {
++ throw new IllegalArgumentException(path + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!");
++ }
++
++ return type.getConfig(file).getName();
++ }
++ }
++
++ private class UpdateFileVisitor implements FileVisitor<Path> {
++
++ private final String targetName;
++ @Nullable
++ private Path validPlugin;
++
++ private UpdateFileVisitor(String targetName) {
++ this.targetName = targetName;
++ }
++
++ @Override
++ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
++ return FileVisitResult.CONTINUE;
++ }
++
++ @Override
++ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
++ try {
++ String updatePluginName = FileProviderSource.this.getPluginName(file);
++ if (this.targetName.equals(updatePluginName)) {
++ this.validPlugin = file;
++ return FileVisitResult.TERMINATE;
++ }
++ } catch (Exception e) {
++ // We failed to load this data for some reason, so, we'll skip over this
++ }
++
++
++ return FileVisitResult.CONTINUE;
++ }
++
++ @Override
++ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
++ return FileVisitResult.CONTINUE;
++ }
++
++ @Override
++ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
++ return FileVisitResult.CONTINUE;
++ }
++
++ @Nullable
++ public Path getValidPlugin() {
++ return validPlugin;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ac55ae0e30119556f01e2e36c20fc63a111fae5f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+@@ -0,0 +1,43 @@
++package io.papermc.paper.plugin.provider.source;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import org.slf4j.Logger;
++
++import java.util.List;
++
++/**
++ * Registers providers at the provided files in the add-plugin argument.
++ */
++public class PluginFlagProviderSource implements ProviderSource<List<Path>, List<Path>> {
++
++ public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource();
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted);
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ @Override
++ public List<Path> prepareContext(List<Path> context) {
++ final List<Path> files = new ArrayList<>();
++ for (Path path : context) {
++ try {
++ files.add(FILE_PROVIDER_SOURCE.prepareContext(path));
++ } catch (Exception e) {
++ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e);
++ }
++ }
++ return files;
++ }
++
++ @Override
++ public void registerProviders(EntrypointHandler entrypointHandler, List<Path> context) {
++ for (Path path : context) {
++ try {
++ FILE_PROVIDER_SOURCE.registerProviders(entrypointHandler, path);
++ } catch (Exception e) {
++ LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6b09813c75fad02fe9b8deb0bf86ad0b148fa770
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
+@@ -0,0 +1,32 @@
++package io.papermc.paper.plugin.provider.source;
++
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import java.io.IOException;
++
++/**
++ * A provider source is responsible for giving PluginTypes an EntrypointHandler for
++ * registering providers at.
++ *
++ * @param <I> input context
++ * @param <C> context
++ */
++public interface ProviderSource<I, C> {
++
++ /**
++ * Prepares the context for use in {@link #registerProviders(EntrypointHandler, Object)}.
++ *
++ * @param context the context to prepare
++ * @return the prepared context, ready for use in {@link #registerProviders(EntrypointHandler, Object)}
++ * @throws IOException if an error occurs while preparing the context
++ */
++ C prepareContext(I context) throws IOException;
++
++ /**
++ * Uses the prepared context to register providers at the given entrypoint handler.
++ *
++ * @param entrypointHandler the entrypoint handler to register providers at
++ * @param context the context to register providers at
++ * @throws Exception if an error occurs while registering providers
++ */
++ void registerProviders(EntrypointHandler entrypointHandler, C context) throws Exception;
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..87128685015d550440a798028f50be24bc755f6c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+@@ -0,0 +1,85 @@
++package io.papermc.paper.plugin.provider.type;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.jetbrains.annotations.Nullable;
++
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++/**
++ * This is where spigot/paper plugins are registered.
++ * This will get the jar and find a certain config file, create an object
++ * then registering it into a {@link EntrypointHandler} at a certain {@link Entrypoint}.
++ */
++public abstract class PluginFileType<T, C extends PluginMeta> {
++
++ private static final List<String> CONFIG_TYPES = new ArrayList<>();
++
++ public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) {
++ @Override
++ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) {
++ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null;
++ if (parent.shouldCreateBootstrap()) {
++ bootstrapPluginProvider = parent.createBootstrapProvider();
++ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider);
++ }
++
++ entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider));
++ }
++ };
++ public static final PluginFileType<SpigotPluginProvider, PluginDescriptionFile> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) {
++ @Override
++ protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) {
++ entrypointHandler.register(Entrypoint.PLUGIN, provider);
++ }
++ };
++
++ private static final List<PluginFileType<?, ?>> VALUES = List.of(PAPER, SPIGOT);
++
++ private final String config;
++ private final PluginTypeFactory<T, C> factory;
++
++ PluginFileType(String config, PluginTypeFactory<T, C> factory) {
++ this.config = config;
++ this.factory = factory;
++ CONFIG_TYPES.add(config);
++ }
++
++ @Nullable
++ public static PluginFileType<?, ?> guessType(JarFile file) {
++ for (PluginFileType<?, ?> type : VALUES) {
++ JarEntry entry = file.getJarEntry(type.config);
++ if (entry != null) {
++ return type;
++ }
++ }
++
++ return null;
++ }
++
++ public T register(EntrypointHandler entrypointHandler, JarFile file, Path context) throws Exception {
++ C config = this.getConfig(file);
++ T provider = this.factory.build(file, config, context);
++ this.register(entrypointHandler, provider);
++ return provider;
++ }
++
++ public C getConfig(JarFile file) throws Exception {
++ return this.factory.create(file, file.getJarEntry(this.config));
++ }
++
++ protected abstract void register(EntrypointHandler entrypointHandler, T provider);
++
++ public static List<String> getConfigTypes() {
++ return CONFIG_TYPES;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..32f230d66f6953520b59ccbf3079c5a6242ca92c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.plugin.provider.type;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++
++import java.nio.file.Path;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++/**
++ * A plugin type factory is responsible for building an object
++ * and config for a certain plugin type.
++ *
++ * @param <T> plugin provider type (may not be a plugin provider)
++ * @param <C> config type
++ */
++public interface PluginTypeFactory<T, C extends PluginMeta> {
++
++ T build(JarFile file, C configuration, Path source) throws Exception;
++
++ C create(JarFile file, JarEntry config) throws Exception;
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7750741b6a4a774986d833919510770b593ec7b9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperBootstrapOrderConfiguration.java
+@@ -0,0 +1,50 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++public class PaperBootstrapOrderConfiguration implements LoadOrderConfiguration {
++
++ private final PaperPluginMeta paperPluginMeta;
++ private final List<String> loadBefore = new ArrayList<>();
++ private final List<String> loadAfter = new ArrayList<>();
++
++ public PaperBootstrapOrderConfiguration(PaperPluginMeta paperPluginMeta) {
++ this.paperPluginMeta = paperPluginMeta;
++
++ for (Map.Entry<String, DependencyConfiguration> configuration : paperPluginMeta.getBootstrapDependencies().entrySet()) {
++ String name = configuration.getKey();
++ DependencyConfiguration dependencyConfiguration = configuration.getValue();
++
++ if (dependencyConfiguration.load() == DependencyConfiguration.LoadOrder.AFTER) {
++ // This plugin will load BEFORE all dependencies (so dependencies will load AFTER plugin)
++ this.loadBefore.add(name);
++ } else if (dependencyConfiguration.load() == DependencyConfiguration.LoadOrder.BEFORE) {
++ // This plugin will load AFTER all dependencies (so dependencies will load BEFORE plugin)
++ this.loadAfter.add(name);
++ }
++ }
++ }
++
++ @Override
++ public @NotNull List<String> getLoadBefore() {
++ return this.loadBefore;
++ }
++
++ @Override
++ public @NotNull List<String> getLoadAfter() {
++ return this.loadAfter;
++ }
++
++ @Override
++ public @NotNull PluginMeta getMeta() {
++ return this.paperPluginMeta;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b7e8a5ba375a558e0442aa9facf96954a9bb135f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperLoadOrderConfiguration.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.ArrayList;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++
++public class PaperLoadOrderConfiguration implements LoadOrderConfiguration {
++
++ private final PaperPluginMeta meta;
++ private final List<String> loadBefore;
++ private final List<String> loadAfter;
++
++ public PaperLoadOrderConfiguration(PaperPluginMeta meta) {
++ this.meta = meta;
++
++ this.loadBefore = this.meta.getLoadBeforePlugins();
++ this.loadAfter = this.meta.getLoadAfterPlugins();
++ }
++
++ @Override
++ public @NotNull List<String> getLoadBefore() {
++ return this.loadBefore;
++ }
++
++ @Override
++ public @NotNull List<String> getLoadAfter() {
++ return this.loadAfter;
++ }
++
++ @Override
++ public @NotNull PluginMeta getMeta() {
++ return this.meta;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..55a6898e95704cddafda1ca5dc0951c7102fe10b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
+@@ -0,0 +1,264 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import io.papermc.paper.plugin.provider.util.ProviderUtil;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++import java.util.jar.JarFile;
++
++public class PaperPluginParent {
++
++ public static final PluginTypeFactory<PaperPluginParent, PaperPluginMeta> FACTORY = new PaperPluginProviderFactory();
++ private final Path path;
++ private final JarFile jarFile;
++ private final PaperPluginMeta description;
++ private final PaperPluginClassLoader classLoader;
++ private final PluginProviderContext context;
++ private final ComponentLogger logger;
++
++ public PaperPluginParent(Path path, JarFile jarFile, PaperPluginMeta description, PaperPluginClassLoader classLoader, PluginProviderContext context) {
++ this.path = path;
++ this.jarFile = jarFile;
++ this.description = description;
++ this.classLoader = classLoader;
++ this.context = context;
++ this.logger = context.getLogger();
++ }
++
++ public boolean shouldCreateBootstrap() {
++ return this.description.getBootstrapper() != null;
++ }
++
++ public PaperBootstrapProvider createBootstrapProvider() {
++ return new PaperBootstrapProvider();
++ }
++
++ public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) {
++ return new PaperServerPluginProvider(provider);
++ }
++
++ public class PaperBootstrapProvider implements PluginProvider<PluginBootstrap>, ProviderStatusHolder, DependencyContextHolder {
++
++ private ProviderStatus status;
++ private PluginBootstrap lastProvided;
++
++ @Override
++ public @NotNull Path getSource() {
++ return PaperPluginParent.this.path;
++ }
++
++ @Override
++ public JarFile file() {
++ return PaperPluginParent.this.jarFile;
++ }
++
++ @Override
++ public PluginBootstrap createInstance() {
++ PluginBootstrap bootstrap = ProviderUtil.loadClass(PaperPluginParent.this.description.getBootstrapper(),
++ PluginBootstrap.class, PaperPluginParent.this.classLoader, () -> this.status = ProviderStatus.ERRORED);
++ this.status = ProviderStatus.INITIALIZED;
++ this.lastProvided = bootstrap;
++ return bootstrap;
++ }
++
++ @Override
++ public PaperPluginMeta getMeta() {
++ return PaperPluginParent.this.description;
++ }
++
++ @Override
++ public ComponentLogger getLogger() {
++ return PaperPluginParent.this.logger;
++ }
++
++ @Override
++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) {
++ return new PaperBootstrapOrderConfiguration(PaperPluginParent.this.description);
++ }
++
++ @Override
++ public List<String> validateDependencies(@NotNull DependencyContext context) {
++ List<String> missingDependencies = new ArrayList<>();
++ for (Map.Entry<String, DependencyConfiguration> configuration : this.getMeta().getBootstrapDependencies().entrySet()) {
++ String dependency = configuration.getKey();
++ if (configuration.getValue().required() && !context.hasDependency(dependency)) {
++ missingDependencies.add(dependency);
++ }
++ }
++
++ return missingDependencies;
++ }
++
++ @Override
++ public ProviderStatus getLastProvidedStatus() {
++ return this.status;
++ }
++
++ @Override
++ public void setStatus(ProviderStatus status) {
++ this.status = status;
++ }
++
++ public PluginBootstrap getLastProvided() {
++ return this.lastProvided;
++ }
++
++ @Override
++ public void setContext(DependencyContext context) {
++ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
++ }
++
++ @Override
++ public String toString() {
++ return "PaperBootstrapProvider{" +
++ "parent=" + PaperPluginParent.this +
++ "status=" + status +
++ ", lastProvided=" + lastProvided +
++ '}';
++ }
++ }
++
++ public class PaperServerPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
++
++ private final PaperBootstrapProvider bootstrapProvider;
++
++ private ProviderStatus status;
++
++ PaperServerPluginProvider(PaperBootstrapProvider bootstrapProvider) {
++ this.bootstrapProvider = bootstrapProvider;
++ }
++
++ @Override
++ public @NotNull Path getSource() {
++ return PaperPluginParent.this.path;
++ }
++
++ @Override
++ public JarFile file() {
++ return PaperPluginParent.this.jarFile;
++ }
++
++ @Override
++ public JavaPlugin createInstance() {
++ PluginBootstrap bootstrap = null;
++ if (this.bootstrapProvider != null && this.bootstrapProvider.getLastProvided() != null) {
++ bootstrap = this.bootstrapProvider.getLastProvided();
++ }
++
++ try {
++ JavaPlugin plugin;
++ if (bootstrap == null) {
++ plugin = ProviderUtil.loadClass(PaperPluginParent.this.description.getMainClass(), JavaPlugin.class, PaperPluginParent.this.classLoader);
++ } else {
++ plugin = bootstrap.createPlugin(PaperPluginParent.this.context);
++ }
++
++ if (!plugin.getClass().isAssignableFrom(Class.forName(PaperPluginParent.this.description.getMainClass(), true, plugin.getClass().getClassLoader()))) {
++ logger.info("Bootstrap of plugin " + PaperPluginParent.this.description.getName() + " provided a plugin instance of class " + plugin.getClass().getName() + " which does not match the plugin declared main class");
++ }
++
++ this.status = ProviderStatus.INITIALIZED;
++ return plugin;
++ } catch (Throwable throwable) {
++ this.status = ProviderStatus.ERRORED;
++ SneakyThrow.sneaky(throwable);
++ }
++
++ throw new AssertionError(); // Impossible
++ }
++
++ @Override
++ public PaperPluginMeta getMeta() {
++ return PaperPluginParent.this.description;
++ }
++
++ @Override
++ public ComponentLogger getLogger() {
++ return PaperPluginParent.this.logger;
++ }
++
++ @Override
++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) {
++ return new PaperLoadOrderConfiguration(PaperPluginParent.this.description);
++ }
++
++ @Override
++ public List<String> validateDependencies(@NotNull DependencyContext context) {
++ List<String> missingDependencies = new ArrayList<>();
++ for (Map.Entry<String, DependencyConfiguration> dependency : this.getMeta().getServerDependencies().entrySet()) {
++ String name = dependency.getKey();
++ if (dependency.getValue().required() && !context.hasDependency(name)) {
++ missingDependencies.add(name);
++ }
++ }
++
++ return missingDependencies;
++ }
++
++ @Override
++ public ProviderStatus getLastProvidedStatus() {
++ return this.status;
++ }
++
++ @Override
++ public void setStatus(ProviderStatus status) {
++ this.status = status;
++ }
++
++ public boolean shouldSkipCreation() {
++ if (this.bootstrapProvider == null) {
++ return false;
++ }
++
++ return this.bootstrapProvider.getLastProvidedStatus() == ProviderStatus.ERRORED;
++ }
++
++ /*
++ The plugin has to reuse the classloader in order to share the bootstrapper.
++ However, a plugin may have totally separate dependencies during bootstrapping.
++ This is a bit yuck, but in general we have to treat bootstrapping and normal game as connected.
++ */
++ @Override
++ public void setContext(DependencyContext context) {
++ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
++ }
++
++ @Override
++ public String toString() {
++ return "PaperServerPluginProvider{" +
++ "parent=" + PaperPluginParent.this +
++ "bootstrapProvider=" + bootstrapProvider +
++ ", status=" + status +
++ '}';
++ }
++ }
++
++
++ @Override
++ public String toString() {
++ return "PaperPluginParent{" +
++ "path=" + path +
++ ", jarFile=" + jarFile +
++ ", description=" + description +
++ ", classLoader=" + classLoader +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0a27b468560ccf4b9588cd12d50c02e442f3024f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
+@@ -0,0 +1,56 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import com.destroystokyo.paper.utils.PaperPluginLogger;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader;
++import io.papermc.paper.plugin.loader.PaperClasspathBuilder;
++import io.papermc.paper.plugin.loader.PluginLoader;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import io.papermc.paper.plugin.provider.util.ProviderUtil;
++
++import java.io.BufferedReader;
++import java.io.IOException;
++import java.io.InputStreamReader;
++import java.nio.file.Path;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++
++class PaperPluginProviderFactory implements PluginTypeFactory<PaperPluginParent, PaperPluginMeta> {
++
++ @Override
++ public PaperPluginParent build(JarFile file, PaperPluginMeta configuration, Path source) {
++ Logger jul = PaperPluginLogger.getLogger(configuration);
++ ComponentLogger logger = ComponentLogger.logger(jul.getName());
++ PluginProviderContext context = PluginProviderContextImpl.create(configuration, logger, source);
++
++ PaperClasspathBuilder builder = new PaperClasspathBuilder(context);
++
++ if (configuration.getLoader() != null) {
++ try (
++ PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader())
++ ) {
++ PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader);
++ loader.classloader(builder);
++ } catch (IOException e) {
++ throw new RuntimeException(e);
++ }
++ }
++
++ PaperPluginClassLoader classLoader = builder.buildClassLoader(jul, source, file, configuration);
++ return new PaperPluginParent(source, file, configuration, classLoader, context);
++ }
++
++ @Override
++ public PaperPluginMeta create(JarFile file, JarEntry config) throws IOException {
++ PaperPluginMeta configuration;
++ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) {
++ configuration = PaperPluginMeta.create(bufferedReader);
++ }
++ return configuration;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b2a6544e321fa61c58bdf5684231de1020884fcc
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotLoadOrderConfiguration.java
+@@ -0,0 +1,72 @@
++package io.papermc.paper.plugin.provider.type.spigot;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++
++public class SpigotLoadOrderConfiguration implements LoadOrderConfiguration {
++
++ private final PluginDescriptionFile meta;
++ private final List<String> loadBefore;
++ private final List<String> loadAfter;
++
++ public SpigotLoadOrderConfiguration(SpigotPluginProvider spigotPluginProvider, Map<String, PluginProvider<?>> toLoad) {
++ this.meta = spigotPluginProvider.getMeta();
++
++ this.loadBefore = meta.getLoadBeforePlugins();
++ this.loadAfter = new ArrayList<>();
++ this.loadAfter.addAll(meta.getDepend());
++ this.loadAfter.addAll(meta.getSoftDepend());
++
++ // First: Remove as load after IF already in loadbefore
++ // Some plugins would put a plugin both in depends and in loadbefore,
++ // so in this case, we just ignore the effects of depend.
++ for (String loadBefore : this.loadBefore) {
++ this.loadAfter.remove(loadBefore);
++ }
++
++ // Second: Do a basic check to see if any other dependencies refer back to this plugin.
++ Iterator<String> iterators = this.loadAfter.iterator();
++ while (iterators.hasNext()) {
++ String loadAfter = iterators.next();
++ PluginProvider<?> provider = toLoad.get(loadAfter);
++ if (provider != null) {
++ PluginMeta configuration = provider.getMeta();
++ // Does a configuration refer back to this plugin?
++ Set<String> dependencies = new HashSet<>();
++ dependencies.addAll(configuration.getPluginDependencies());
++ dependencies.addAll(configuration.getPluginSoftDependencies());
++
++ if (configuration.getName().equals(this.meta.getName()) || dependencies.contains(this.meta.getName())) {
++ iterators.remove(); // Let the other config deal with it
++ }
++ }
++ }
++
++ }
++
++ @Override
++ public @NotNull List<String> getLoadBefore() {
++ return this.loadBefore;
++ }
++
++ @Override
++ public @NotNull List<String> getLoadAfter() {
++ return this.loadAfter;
++ }
++
++ @Override
++ public @NotNull PluginMeta getMeta() {
++ return this.meta;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..75a2b687d58d76b94f8bec111df8613f120ff74b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
+@@ -0,0 +1,197 @@
++package io.papermc.paper.plugin.provider.type.spigot;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import com.destroystokyo.paper.utils.PaperPluginLogger;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.bukkit.plugin.java.LibraryLoader;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.File;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.jar.JarFile;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++public class SpigotPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
++
++ public static final PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> FACTORY = new SpigotPluginProviderFactory();
++ private static final LibraryLoader LIBRARY_LOADER = new LibraryLoader(Logger.getLogger("SpigotLibraryLoader"));
++ private final Path path;
++ private final PluginDescriptionFile description;
++ private final JarFile jarFile;
++ private final Logger logger;
++ private final ComponentLogger componentLogger;
++ private ProviderStatus status;
++ private DependencyContext dependencyContext;
++
++ SpigotPluginProvider(Path path, JarFile file, PluginDescriptionFile description) {
++ this.path = path;
++ this.jarFile = file;
++ this.description = description;
++ this.logger = PaperPluginLogger.getLogger(description);
++ this.componentLogger = ComponentLogger.logger(this.logger.getName());
++ }
++
++ @Override
++ public @NotNull Path getSource() {
++ return this.path;
++ }
++
++ @Override
++ public JarFile file() {
++ return this.jarFile;
++ }
++
++ @Override
++ public JavaPlugin createInstance() {
++ Server server = Bukkit.getServer();
++ try {
++
++ final File parentFile = server.getPluginsFolder(); // Paper
++ final File dataFolder = new File(parentFile, this.description.getName());
++ @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, this.description.getRawName());
++
++ // Found old data folder
++ if (dataFolder.equals(oldDataFolder)) {
++ // They are equal -- nothing needs to be done!
++ } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) {
++ server.getLogger().warning(String.format(
++ "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'",
++ this.description.getFullName(),
++ this.path,
++ oldDataFolder,
++ dataFolder
++ ));
++ } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) {
++ if (!oldDataFolder.renameTo(dataFolder)) {
++ throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'");
++ }
++ server.getLogger().log(Level.INFO, String.format(
++ "While loading %s (%s) renamed data folder: `%s' to `%s'",
++ this.description.getFullName(),
++ this.path,
++ oldDataFolder,
++ dataFolder
++ ));
++ }
++
++ if (dataFolder.exists() && !dataFolder.isDirectory()) {
++ throw new InvalidPluginException(String.format(
++ "Projected datafolder: `%s' for %s (%s) exists and is not a directory",
++ dataFolder,
++ this.description.getFullName(),
++ this.path
++ ));
++ }
++
++ Set<String> missingHardDependencies = new HashSet<>(this.description.getDepend().size()); // Paper - list all missing hard depends
++ for (final String pluginName : this.description.getDepend()) {
++ if (!this.dependencyContext.hasDependency(pluginName)) {
++ missingHardDependencies.add(pluginName); // Paper - list all missing hard depends
++ }
++ }
++ // Paper start - list all missing hard depends
++ if (!missingHardDependencies.isEmpty()) {
++ throw new UnknownDependencyException(missingHardDependencies, this.description.getFullName());
++ }
++ // Paper end
++
++ server.getUnsafe().checkSupported(this.description);
++
++ final PluginClassLoader loader;
++ try {
++ loader = new PluginClassLoader(this.getClass().getClassLoader(), this.description, dataFolder, this.path.toFile(), LIBRARY_LOADER.createLoader(this.description), this.jarFile, this.dependencyContext); // Paper
++ } catch (InvalidPluginException ex) {
++ throw ex;
++ } catch (Throwable ex) {
++ throw new InvalidPluginException(ex);
++ }
++
++ // Override dependency context.
++ // We must provide a temporary context in order to properly handle dependencies on the plugin classloader constructor.
++ loader.dependencyContext = PaperPluginManagerImpl.getInstance();
++
++
++ this.status = ProviderStatus.INITIALIZED;
++ return loader.getPlugin();
++ } catch (Throwable ex) {
++ this.status = ProviderStatus.ERRORED;
++ SneakyThrow.sneaky(ex);
++ }
++
++ throw new AssertionError(); // Shouldn't happen
++ }
++
++ @Override
++ public PluginDescriptionFile getMeta() {
++ return this.description;
++ }
++
++ @Override
++ public ComponentLogger getLogger() {
++ return this.componentLogger;
++ }
++
++ @Override
++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) {
++ return new SpigotLoadOrderConfiguration(this, toLoad);
++ }
++
++ @Override
++ public List<String> validateDependencies(@NotNull DependencyContext context) {
++ List<String> missingDependencies = new ArrayList<>();
++ for (String hardDependency : this.getMeta().getPluginDependencies()) {
++ if (!context.hasDependency(hardDependency)) {
++ missingDependencies.add(hardDependency);
++ }
++ }
++
++ return missingDependencies;
++ }
++
++ @Override
++ public ProviderStatus getLastProvidedStatus() {
++ return this.status;
++ }
++
++ @Override
++ public void setStatus(ProviderStatus status) {
++ this.status = status;
++ }
++
++ @Override
++ public void setContext(DependencyContext context) {
++ this.dependencyContext = context;
++ }
++
++ @Override
++ public String toString() {
++ return "SpigotPluginProvider{" +
++ "path=" + path +
++ ", description=" + description +
++ ", jarFile=" + jarFile +
++ ", status=" + status +
++ ", dependencyContext=" + dependencyContext +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bdd9bc8a414719b9f1d6f01f90539ddb8603a878
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+@@ -0,0 +1,45 @@
++package io.papermc.paper.plugin.provider.type.spigot;
++
++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.yaml.snakeyaml.error.YAMLException;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.nio.file.Path;
++import java.util.Locale;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
++
++ @Override
++ public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
++ // Copied from SimplePluginManager#loadPlugins
++ // Spigot doesn't validate the name when the config is created, and instead when the plugin is loaded.
++ // Paper plugin configuration will do these checks in config serializer instead of when this is created.
++ String name = configuration.getRawName();
++ if (PluginConfigConstraints.RESERVED_KEYS.contains(name.toLowerCase(Locale.ROOT))) {
++ throw new InvalidDescriptionException("Restricted name, cannot use %s as a plugin name.".formatted(name));
++ } else if (name.indexOf(' ') != -1) {
++ throw new InvalidDescriptionException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
++ }
++
++ return new SpigotPluginProvider(source, file, configuration);
++ }
++
++ @Override
++ public PluginDescriptionFile create(JarFile file, JarEntry config) throws InvalidDescriptionException {
++ PluginDescriptionFile descriptionFile;
++ try (InputStream inputStream = file.getInputStream(config)) {
++ descriptionFile = new PluginDescriptionFile(inputStream);
++ } catch (IOException | YAMLException ex) {
++ throw new InvalidDescriptionException(ex);
++ }
++
++ return descriptionFile;
++ }
++}
++
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2e96308696e131f3f013469a395e5ddda2c5d529
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
+@@ -0,0 +1,65 @@
++package io.papermc.paper.plugin.storage;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import io.papermc.paper.plugin.bootstrap.PluginBootstrapContextImpl;
++import io.papermc.paper.plugin.entrypoint.dependency.BootstrapMetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import org.slf4j.Logger;
++
++public class BootstrapProviderStorage extends SimpleProviderStorage<PluginBootstrap> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ public BootstrapProviderStorage() {
++ super(new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++ @Override
++ public void applyContext(PluginProvider<PluginBootstrap> provider, DependencyContext dependencyContext) {
++ if (provider instanceof DependencyContextHolder contextHolder) {
++ contextHolder.setContext(dependencyContext);
++ }
++ }
++
++ @Override
++ public boolean load(PluginProvider<PluginBootstrap> provider, PluginBootstrap provided) {
++ try {
++ BootstrapContext context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath());
++ provided.bootstrap(context);
++ return true;
++ } catch (Throwable e) {
++ LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e);
++ if (provider instanceof ProviderStatusHolder statusHolder) {
++ statusHolder.setStatus(ProviderStatus.ERRORED);
++ }
++ return false;
++ }
++ }
++
++ @Override
++ public void onGenericError(PluginProvider<PluginBootstrap> provider) {
++ if (provider instanceof ProviderStatusHolder statusHolder) {
++ statusHolder.setStatus(ProviderStatus.ERRORED);
++ }
++ }
++ }));
++ }
++
++ @Override
++ public MetaDependencyTree createDependencyTree() {
++ return new BootstrapMetaDependencyTree();
++ }
++
++ @Override
++ public String toString() {
++ return "BOOTSTRAP:" + super.toString();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8ef4806cadabe56264dd861f1a1854b2354b3b5c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
+@@ -0,0 +1,17 @@
++package io.papermc.paper.plugin.storage;
++
++import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++
++public abstract class ConfiguredProviderStorage<T> extends SimpleProviderStorage<T> {
++
++ public static final boolean LEGACY_PLUGIN_LOADING = Boolean.getBoolean("paper.useLegacyPluginLoading");
++
++ protected ConfiguredProviderStorage(ProviderConfiguration<T> onLoad) {
++ // This doesn't work with reloading.
++ // Should we care?
++ super(LEGACY_PLUGIN_LOADING ? new LegacyPluginLoadingStrategy<>(onLoad) : new ModernPluginLoadingStrategy<>(onLoad));
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..39cd3acd3f76b3b0d065e0efb04a3eacea1c2e6b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.plugin.storage;
++
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++/**
++ * A provider storage is meant to be a singleton that stores providers.
++ *
++ * @param <T> provider type
++ */
++public interface ProviderStorage<T> {
++
++ void register(PluginProvider<T> provider);
++
++ MetaDependencyTree createDependencyTree();
++
++ void enter();
++
++ Iterable<PluginProvider<T>> getRegisteredProviders();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cb9b13522a976b82bcb71cef486f11f4172e3e99
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
+@@ -0,0 +1,70 @@
++package io.papermc.paper.plugin.storage;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.slf4j.Logger;
++
++import java.util.List;
++
++public class ServerPluginProviderStorage extends ConfiguredProviderStorage<JavaPlugin> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ public ServerPluginProviderStorage() {
++ super(new ProviderConfiguration<>() {
++ @Override
++ public void applyContext(PluginProvider<JavaPlugin> provider, DependencyContext dependencyContext) {
++ Plugin alreadyLoadedPlugin = PaperPluginManagerImpl.getInstance().getPlugin(provider.getMeta().getName());
++ if (alreadyLoadedPlugin != null) {
++ throw new IllegalStateException("Provider " + provider + " attempted to add duplicate plugin identifier " + alreadyLoadedPlugin + " THIS WILL CREATE BUGS!!!");
++ }
++
++ if (provider instanceof DependencyContextHolder contextHolder) {
++ contextHolder.setContext(dependencyContext);
++ }
++ }
++
++ @Override
++ public boolean load(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
++ // Add it to the map here, we have to run the actual loading logic later.
++ PaperPluginManagerImpl.getInstance().loadPlugin(provided);
++ return true;
++ }
++ });
++ }
++
++ @Override
++ protected void filterLoadingProviders(List<PluginProvider<JavaPlugin>> pluginProviders) {
++ /*
++ Have to do this to prevent loading plugin providers that have failed initializers.
++ This is a hack and a better solution here would be to store failed plugin providers elsewhere.
++ */
++ pluginProviders.removeIf((provider) -> (provider instanceof PaperPluginParent.PaperServerPluginProvider pluginProvider && pluginProvider.shouldSkipCreation()));
++ }
++
++ // We need to call the load methods AFTER all plugins are constructed
++ @Override
++ public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
++ try {
++ provided.getLogger().info(String.format("Loading server plugin %s", provided.getPluginMeta().getDisplayName()));
++ provided.onLoad();
++ } catch (Throwable ex) {
++ // Don't mark that provider as ERRORED, as this apparently still needs to run the onEnable logic.
++ provided.getSLF4JLogger().error("Error initializing plugin '%s' in folder '%s' (Is it up to date?)".formatted(provider.getFileName(), provider.getParentSource()), ex);
++ }
++ }
++
++ @Override
++ public String toString() {
++ return "PLUGIN:" + super.toString();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..26422904751647a061397ce978bba752149003cd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
+@@ -0,0 +1,93 @@
++package io.papermc.paper.plugin.storage;
++
++import com.google.common.graph.GraphBuilder;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.stream.Collectors;
++
++public abstract class SimpleProviderStorage<T> implements ProviderStorage<T> {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ protected final List<PluginProvider<T>> providers = new ArrayList<>();
++ protected ProviderLoadingStrategy<T> strategy;
++
++ protected SimpleProviderStorage(ProviderLoadingStrategy<T> strategy) {
++ this.strategy = strategy;
++ }
++
++ @Override
++ public void register(PluginProvider<T> provider) {
++ this.providers.add(provider);
++ }
++
++ @Override
++ public void enter() {
++ List<PluginProvider<T>> providerList = new ArrayList<>(this.providers);
++ this.filterLoadingProviders(providerList);
++
++ try {
++ for (ProviderLoadingStrategy.ProviderPair<T> providerPair : this.strategy.loadProviders(providerList, this.createDependencyTree())) {
++ this.processProvided(providerPair.provider(), providerPair.provided());
++ }
++ } catch (PluginGraphCycleException exception) {
++ this.handleCycle(exception);
++ }
++ }
++
++ @Override
++ public MetaDependencyTree createDependencyTree() {
++ return new SimpleMetaDependencyTree(GraphBuilder.directed().build());
++ }
++
++ @Override
++ public Iterable<PluginProvider<T>> getRegisteredProviders() {
++ return this.providers;
++ }
++
++ public void processProvided(PluginProvider<T> provider, T provided) {
++ }
++
++ // Mutable enter
++ protected void filterLoadingProviders(List<PluginProvider<T>> providers) {
++ }
++
++ protected void handleCycle(PluginGraphCycleException exception) {
++ List<String> logMessages = new ArrayList<>();
++ for (List<String> list : exception.getCycles()) {
++ logMessages.add(String.join(" -> ", list) + " -> " + list.get(0));
++ }
++
++ LOGGER.error("Circular plugin loading detected!");
++ LOGGER.error("Circular load order:");
++ for (String logMessage : logMessages) {
++ LOGGER.error(" {}", logMessage);
++ }
++ LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
++ LOGGER.error("If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true");
++
++ if (this.throwOnCycle()) {
++ throw new IllegalStateException("Circular plugin loading from plugins " + exception.getCycles().stream().map(cycle -> cycle.get(0)).collect(Collectors.joining(", ")));
++ }
++ }
++
++ public boolean throwOnCycle() {
++ return true;
++ }
++
++ @Override
++ public String toString() {
++ return "SimpleProviderStorage{" +
++ "providers=" + this.providers +
++ ", strategy=" + this.strategy +
++ '}';
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/package-info.java b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c1114675137e862ac9682b635bfdbfbc1d7c6e67
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
+@@ -0,0 +1,5 @@
++/**
++ * Classes in this package are supposed to connect components of {@link io.papermc.paper.plugin.entrypoint} and {@link io.papermc.paper.plugin.provider} packages.
++ * @see io.papermc.paper.plugin.entrypoint.Entrypoint
++ */
++package io.papermc.paper.plugin.storage;
+diff --git a/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..01c88a23755618b98c1a1cdeb8e404e79401940b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.plugin.util;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.source.ProviderSource;
++import org.slf4j.Logger;
++
++public final class EntrypointUtil {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ public static <I, C> void registerProvidersFromSource(ProviderSource<I, C> source, I contextInput) {
++ try {
++ C context = source.prepareContext(contextInput);
++ source.registerProviders(LaunchEntryPointHandler.INSTANCE, context);
++ } catch (Throwable e) {
++ LOGGER.error(e.getMessage(), e);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fd55fd1d6518ebd1bc2513dd331f072018fd4782
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
+@@ -0,0 +1,37 @@
++package io.papermc.paper.plugin.util;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++public class NamespaceChecker {
++
++ private static final String[] QUICK_INVALID_NAMESPACES = {
++ "net.minecraft.",
++ "org.bukkit.",
++ "io.papermc.paper.",
++ "com.destroystokoyo.paper."
++ };
++
++ /**
++ * Used for a variety of namespaces that shouldn't be resolved and should instead be moved to
++ * other classloaders. We can assume this because only plugins should be using this classloader.
++ *
++ * @param name namespace
++ */
++ public static void validateNameSpaceForClassloading(@NotNull String name) throws ClassNotFoundException {
++ if (!isValidNameSpace(name)) {
++ throw new ClassNotFoundException(name);
++ }
++ }
++
++ public static boolean isValidNameSpace(@NotNull String name) {
++ for (String string : QUICK_INVALID_NAMESPACES) {
++ if (name.startsWith(string)) {
++ return false;
++ }
++ }
++
++ return true;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+index f7114d5b8f2f93f62883e24da29afaf9f74ee1a6..8bf0630c0e06950cd99b7ae9898137f70c22063f 100644
+--- a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
++++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+@@ -1,9 +1,10 @@
+ package io.papermc.paper.util;
+
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+ import org.bukkit.plugin.java.JavaPlugin;
+-import org.bukkit.plugin.java.PluginClassLoader;
+ import org.jetbrains.annotations.Nullable;
+
++import java.util.Objects;
+ import java.util.Optional;
+
+ public class StackWalkerUtil {
+@@ -12,11 +13,18 @@ public class StackWalkerUtil {
+ public static JavaPlugin getFirstPluginCaller() {
+ Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(stream -> stream
+- .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
+ .map((frame) -> {
+- PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
+- return classLoader.getPlugin();
++ ClassLoader classLoader = frame.getDeclaringClass().getClassLoader();
++ JavaPlugin plugin;
++ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
++ plugin = configuredPluginClassLoader.getPlugin();
++ } else {
++ plugin = null;
++ }
++
++ return plugin;
+ })
++ .filter(Objects::nonNull)
+ .findFirst());
+
+ return foundFrame.orElse(null);
+diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+index f66a2154486b6d3b5873da043e51df91cd396c72..3f72e30b57fb2a4231e22a2234729408c1240af4 100644
+--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -330,7 +330,13 @@ public class BuiltInRegistries {
+ }
+
+ public static void bootStrap() {
++ // Paper start
++ bootStrap(() -> {});
++ }
++ public static void bootStrap(Runnable runnable) {
++ // Paper end
+ createContents();
++ runnable.run(); // Paper
+ freeze();
+ validate(REGISTRY);
+ }
+diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java
+index 529d376c168014e41064262b4088bfcef74e8433..01c8d583a4addf43b8cad55a51989de7c3f73d90 100644
+--- a/src/main/java/net/minecraft/server/Bootstrap.java
++++ b/src/main/java/net/minecraft/server/Bootstrap.java
+@@ -66,6 +66,7 @@ public class Bootstrap {
+ Bootstrap.isBootstrapped = true;
+ Instant instant = Instant.now();
+
++ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
+ if (BuiltInRegistries.REGISTRY.keySet().isEmpty()) {
+ throw new IllegalStateException("Unable to load registries");
+ } else {
+@@ -77,7 +78,10 @@ public class Bootstrap {
+ EntitySelectorOptions.bootStrap();
+ DispenseItemBehavior.bootStrap();
+ CauldronInteraction.bootStrap();
+- BuiltInRegistries.bootStrap();
++ // Paper start
++ BuiltInRegistries.bootStrap(() -> {
++ });
++ // Paper end
+ CreativeModeTabs.validate();
+ Bootstrap.wrapStreams();
+ Bootstrap.bootstrapDuration.set(Duration.between(instant, Instant.now()).toMillis());
+diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
+index c791b6d176090d67ecb250c6bc71c90b6c62f447..a9f09f2939a0fbf20be7f8bc27a6d8b961fb748a 100644
+--- a/src/main/java/net/minecraft/server/Main.java
++++ b/src/main/java/net/minecraft/server/Main.java
+@@ -121,6 +121,7 @@ public class Main {
+ JvmProfiler.INSTANCE.start(Environment.SERVER);
+ }
+
++ io.papermc.paper.plugin.PluginInitializerManager.load(optionset); // Paper
+ Bootstrap.bootStrap();
+ Bootstrap.validate();
+ Util.startTimerHackThread();
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 118c8b227133639427c1da84b93fcaa865fd6d02..542ff64ce0cb93a9f996fa0a65e8dde7ed39c3a9 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -280,7 +280,8 @@ public final class CraftServer implements Server {
+ private final CraftCommandMap commandMap = new CraftCommandMap(this);
+ private final SimpleHelpMap helpMap = new SimpleHelpMap(this);
+ private final StandardMessenger messenger = new StandardMessenger();
+- private final SimplePluginManager pluginManager = new SimplePluginManager(this, this.commandMap);
++ private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap);
++ public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); {this.pluginManager.paperPluginManager = this.paperPluginManager;} // Paper
+ private final StructureManager structureManager;
+ protected final DedicatedServer console;
+ protected final DedicatedPlayerList playerList;
+@@ -458,24 +459,7 @@ public final class CraftServer implements Server {
+ }
+
+ public void loadPlugins() {
+- this.pluginManager.registerInterface(JavaPluginLoader.class);
+-
+- File pluginFolder = (File) this.console.options.valueOf("plugins");
+-
+- if (pluginFolder.exists()) {
+- Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder);
+- for (Plugin plugin : plugins) {
+- try {
+- String message = String.format("Loading %s", plugin.getDescription().getFullName());
+- plugin.getLogger().info(message);
+- plugin.onLoad();
+- } catch (Throwable ex) {
+- Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+- }
+- }
+- } else {
+- pluginFolder.mkdir();
+- }
++ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
+ }
+
+ public void enablePlugins(PluginLoadOrder type) {
+@@ -564,15 +548,17 @@ public final class CraftServer implements Server {
+ private void enablePlugin(Plugin plugin) {
+ try {
+ List<Permission> perms = plugin.getDescription().getPermissions();
+-
++ List<Permission> permsToLoad = new ArrayList<>(); // Paper
+ for (Permission perm : perms) {
+- try {
+- this.pluginManager.addPermission(perm, false);
+- } catch (IllegalArgumentException ex) {
+- this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
++ // Paper start
++ if (this.paperPluginManager.getPermission(perm.getName()) == null) {
++ permsToLoad.add(perm);
++ } else {
++ this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered");
++ // Paper end
+ }
+ }
+- this.pluginManager.dirtyPermissibles();
++ this.paperPluginManager.addPermissions(permsToLoad); // Paper
+
+ this.pluginManager.enablePlugin(plugin);
+ } catch (Throwable ex) {
+@@ -1015,6 +1001,7 @@ public final class CraftServer implements Server {
+ "This plugin is not properly shutting down its async tasks when it is being reloaded. This may cause conflicts with the newly loaded version of the plugin"
+ ));
+ }
++ io.papermc.paper.plugin.PluginInitializerManager.reload(this.console); // Paper
+ this.loadPlugins();
+ this.enablePlugins(PluginLoadOrder.STARTUP);
+ this.enablePlugins(PluginLoadOrder.POSTWORLD);
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index bcc9c0295495301d3b62ceb9d4ea93e365caee87..8e747d48fb76c3f8bacb28f270754d5caeca4445 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -402,6 +402,16 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
+ return nmsItemStack.getItem().getDescriptionId();
+ }
++ // Paper start
++ @Override
++ public boolean isSupportedApiVersion(String apiVersion) {
++ if (apiVersion == null) return false;
++ final ApiVersion toCheck = ApiVersion.getOrCreateVersion(apiVersion);
++ final ApiVersion minimumVersion = MinecraftServer.getServer().server.minimumAPI;
++
++ return !toCheck.isNewerThan(ApiVersion.CURRENT) && !toCheck.isOlderThan(minimumVersion);
++ }
++ // Paper end
+
+ @Override
+ public String getTranslationKey(final Attribute attribute) {
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
+new file mode 100644
+index 0000000000000000000000000000000000000000..20dbe2775951bfcdb85c5d679ac86c77a93e0847
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
+@@ -0,0 +1 @@
++io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
+new file mode 100644
+index 0000000000000000000000000000000000000000..a22647244037cd92262b3b5a6582f0a11172fdc8
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
+@@ -0,0 +1 @@
++io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage
+diff --git a/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader
+new file mode 100644
+index 0000000000000000000000000000000000000000..4f78bc62d03460463b9694de933e5b73da8df6e3
+--- /dev/null
++++ b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader
+@@ -0,0 +1 @@
++io.papermc.paper.plugin.manager.DummyBukkitPluginLoader
+diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1d14f530ef888102e47eeeaf0d1a6076e51871c4
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
+@@ -0,0 +1,146 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.bukkit.Server;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.bukkit.configuration.file.FileConfiguration;
++import org.bukkit.generator.BiomeProvider;
++import org.bukkit.generator.ChunkGenerator;
++import org.bukkit.plugin.PluginBase;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.PluginLogger;
++
++import java.io.File;
++import java.io.InputStream;
++import java.util.List;
++
++public class PaperTestPlugin extends PluginBase {
++ private final String pluginName;
++ private boolean enabled = true;
++ private final PluginMeta configuration;
++
++ public PaperTestPlugin(String pluginName) {
++ this.pluginName = pluginName;
++ this.configuration = new TestPluginMeta(pluginName);
++ }
++
++ public PaperTestPlugin(PluginMeta configuration) {
++ this.configuration = configuration;
++ this.pluginName = configuration.getName();
++ }
++
++ @Override
++ public File getDataFolder() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public PluginDescriptionFile getDescription() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public PluginMeta getPluginMeta() {
++ return this.configuration;
++ }
++
++ @Override
++ public FileConfiguration getConfig() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public InputStream getResource(String filename) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void saveConfig() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void saveDefaultConfig() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void saveResource(String resourcePath, boolean replace) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void reloadConfig() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public PluginLogger getLogger() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public PluginLoader getPluginLoader() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public Server getServer() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public boolean isEnabled() {
++ return enabled;
++ }
++
++ public void setEnabled(boolean enabled) {
++ this.enabled = enabled;
++ }
++
++ @Override
++ public void onDisable() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void onLoad() {
++ }
++
++ @Override
++ public void onEnable() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public boolean isNaggable() {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public void setNaggable(boolean canNag) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public BiomeProvider getDefaultBiomeProvider(String worldName, String id) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++
++ @Override
++ public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
++ throw new UnsupportedOperationException("Not supported.");
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f4c6b799d80ea555b41396c4a8fd1e02f9197709
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java
+@@ -0,0 +1,62 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
++import org.bukkit.support.environment.Normal;
++import org.junit.jupiter.api.Test;
++
++import java.util.List;
++
++import static org.hamcrest.MatcherAssert.assertThat;
++
++@Normal
++public class PluginDependencyValidationTest {
++
++ private static final TestPluginMeta MAIN;
++ private static final TestPluginMeta HARD_DEPENDENCY_1;
++ private static final TestPluginMeta SOFT_DEPENDENCY_1;
++
++ public static final String ROOT_NAME = "main";
++
++ public static final String REGISTERED_HARD_DEPEND = "hard1";
++ public static final String REGISTERED_SOFT_DEPEND = "soft1";
++ public static final String UNREGISTERED_HARD_DEPEND = "hard2";
++ public static final String UNREGISTERED_SOFT_DEPEND = "soft2";
++
++ static {
++ MAIN = new TestPluginMeta(ROOT_NAME);
++ MAIN.setSoftDependencies(List.of(REGISTERED_SOFT_DEPEND, UNREGISTERED_SOFT_DEPEND));
++ MAIN.setHardDependencies(List.of(REGISTERED_HARD_DEPEND, UNREGISTERED_HARD_DEPEND));
++
++ HARD_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_HARD_DEPEND);
++ SOFT_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_SOFT_DEPEND);
++ }
++
++ @Test
++ public void testDependencyTree() {
++ MetaDependencyTree tree = new SimpleMetaDependencyTree();
++ tree.add(MAIN);
++ tree.add(HARD_DEPENDENCY_1);
++ tree.add(SOFT_DEPENDENCY_1);
++
++ // Test simple transitive dependencies
++ assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_SOFT_DEPEND), tree.isTransitiveDependency(MAIN, SOFT_DEPENDENCY_1));
++ assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_HARD_DEPEND), tree.isTransitiveDependency(MAIN, HARD_DEPENDENCY_1));
++
++ assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_SOFT_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(SOFT_DEPENDENCY_1, MAIN));
++ assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_HARD_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(HARD_DEPENDENCY_1, MAIN));
++
++ // Test to ensure that registered dependencies exist
++ assertThat("tree did not contain dependency %s".formatted(ROOT_NAME), tree.hasDependency(ROOT_NAME));
++ assertThat("tree did not contain dependency %s".formatted(REGISTERED_HARD_DEPEND), tree.hasDependency(REGISTERED_HARD_DEPEND));
++ assertThat("tree did not contain dependency %s".formatted(REGISTERED_SOFT_DEPEND), tree.hasDependency(REGISTERED_SOFT_DEPEND));
++
++ // Test to ensure unregistered dependencies don't exist
++ assertThat("tree contained dependency %s".formatted(UNREGISTERED_HARD_DEPEND), !tree.hasDependency(UNREGISTERED_HARD_DEPEND));
++ assertThat("tree contained dependency %s".formatted(UNREGISTERED_SOFT_DEPEND), !tree.hasDependency(UNREGISTERED_SOFT_DEPEND));
++
++ // Test removal
++ tree.remove(HARD_DEPENDENCY_1);
++ assertThat("tree contained dependency %s".formatted(REGISTERED_HARD_DEPEND), !tree.hasDependency(REGISTERED_HARD_DEPEND));
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..caa6d514d2ffa2505e804878678d745b689e214d
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java
+@@ -0,0 +1,150 @@
++package io.papermc.paper.plugin;
++
++import com.google.common.graph.GraphBuilder;
++import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.bukkit.support.environment.Normal;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Test;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
++
++@Normal
++public class PluginLoadOrderTest {
++
++ private static List<PluginProvider<PaperTestPlugin>> REGISTERED_PROVIDERS = new ArrayList<>();
++ private static Map<String, Integer> LOAD_ORDER = new HashMap<>();
++ private static final String[] EMPTY = {};
++
++ static {
++ setup();
++ }
++
++ private static TestJavaPluginProvider setup(String identifier, String[] loadAfter, String[] loadAfterSoft, String[] before) {
++ TestPluginMeta configuration = new TestPluginMeta(identifier);
++ configuration.setHardDependencies(List.of(loadAfter));
++ configuration.setSoftDependencies(List.of(loadAfterSoft));
++ configuration.setLoadBefore(List.of(before));
++
++ TestJavaPluginProvider provider = new TestJavaPluginProvider(configuration);
++ REGISTERED_PROVIDERS.add(provider);
++ return provider;
++ }
++
++ /**
++ * Obfuscated plugin names, this uses a real dependency tree...
++ */
++ private static void setup() {
++ setup("RedAir", EMPTY, new String[]{"NightShovel", "EmeraldFire"}, new String[]{"GreenShovel", "IronSpork", "BrightBlueShovel", "WireDoor"});
++ setup("BigGrass", EMPTY, new String[]{"IronEarth", "RedAir"}, new String[]{"BlueFire"});
++ setup("BlueFire", EMPTY, EMPTY, EMPTY);
++ setup("BigPaper", EMPTY, new String[]{"BlueFire"}, EMPTY);
++ setup("EmeraldSpork", EMPTY, EMPTY, new String[]{"GoldPaper", "YellowSnow"});
++ setup("GreenShovel", EMPTY, EMPTY, EMPTY);
++ setup("BrightBlueGrass", new String[]{"BigPaper"}, new String[]{"DarkSpork"}, EMPTY);
++ setup("GoldPaper", EMPTY, new String[]{"BlueFire"}, EMPTY);
++ setup("GreenGlass", EMPTY, EMPTY, EMPTY);
++ setup("GoldNeptune", EMPTY, new String[]{"GreenShovel", "GoldNeptuneVersioning"}, EMPTY);
++ setup("RedPaper", EMPTY, new String[]{"GoldPaper", "GoldFire", "EmeraldGrass", "BlueFire", "CopperSpork", "YellowDoor", "OrangeClam", "BlueSponge", "GoldNeptune", "BrightBlueGrass", "DarkSpoon", "BigShovel", "GreenGlass", "IronGlass"}, new String[]{"IronPaper", "YellowFire"});
++ setup("YellowGrass", EMPTY, new String[]{"RedAir"}, EMPTY);
++ setup("WireFire", EMPTY, new String[]{"RedPaper", "WireGrass", "YellowSpork", "NightAir"}, EMPTY);
++ setup("OrangeNeptune", EMPTY, EMPTY, EMPTY);
++ setup("BigSpoon", new String[]{"YellowGrass", "GreenShovel"}, new String[]{"RedAir", "GoldNeptune", "BrightBlueGrass", "LightDoor", "LightSpork", "LightEarth", "NightDoor", "OrangeSpoon", "GoldSponge", "GoldDoor", "DarkPaper", "RedPaper", "GreenGlass", "IronGlass", "NightGlass", "BigGrass", "BlueFire", "YellowSpoon", "DiamondGrass", "DiamondShovel", "DarkSnow", "EmeraldGlass", "EmeraldSpoon", "LightFire", "WireGrass", "RedEarth", "WireFire"}, EMPTY);
++ setup("CopperSnow", EMPTY, new String[]{"RedSnow", "OrangeFire", "WireAir", "GreenGlass", "NightSpork", "EmeraldPaper"}, new String[]{"BlueGrass"});
++ setup("BrightBluePaper", EMPTY, new String[]{"GoldEarth", "BrightBlueSpoon", "CopperGlass", "LightSporkChat", "DarkAir", "LightEarth", "DiamondDoor", "YellowShovel", "BlueAir", "DarkShovel", "GoldPaper", "BlueFire", "GreenGlass", "YellowSpork", "BigGrass", "OrangePaper", "DarkPaper"}, new String[]{"WireShovel"});
++ setup("LightSponge", EMPTY, EMPTY, EMPTY);
++ setup("OrangeShovel", EMPTY, EMPTY, EMPTY);
++ setup("GoldGrass", EMPTY, new String[]{"GreenGlass", "BlueFire"}, EMPTY);
++ setup("IronSponge", EMPTY, new String[]{"DiamondEarth"}, EMPTY);
++ setup("EmeraldSnow", EMPTY, EMPTY, EMPTY);
++ setup("BlueSpoon", new String[]{"BigGrass"}, new String[]{"GreenGlass", "GoldPaper", "GreenShovel", "YellowClam"}, EMPTY);
++ setup("BigSpork", EMPTY, new String[]{"BigPaper"}, EMPTY);
++ setup("BluePaper", EMPTY, new String[]{"BigClam", "RedSpoon", "GreenFire", "WireSnow", "OrangeSnow", "BlueFire", "BrightBlueGrass", "YellowSpork", "GreenGlass"}, EMPTY);
++ setup("OrangeSpork", EMPTY, EMPTY, EMPTY);
++ setup("DiamondNeptune", EMPTY, new String[]{"GreenGlass", "GreenShovel", "YellowNeptune"}, EMPTY);
++ setup("BigFire", EMPTY, new String[]{"BlueFire", "BrightBlueDoor", "GreenGlass"}, EMPTY);
++ setup("NightNeptune", EMPTY, new String[]{"BlueFire", "DarkGlass", "GoldPaper", "YellowNeptune", "BlueShovel"}, EMPTY);
++ setup("YellowEarth", new String[]{"RedAir"}, EMPTY, EMPTY);
++ setup("DiamondClam", EMPTY, EMPTY, EMPTY);
++ setup("CopperAir", EMPTY, new String[]{"BigPaper"}, EMPTY);
++ setup("NightSpoon", new String[]{"OrangeNeptune"}, new String[]{"BlueFire", "GreenGlass", "RedSpork", "GoldPaper", "BigShovel", "YellowSponge", "EmeraldSpork"}, EMPTY);
++ setup("GreenClam", EMPTY, new String[]{"GreenShovel", "BrightBlueEarth", "BigSpoon", "RedPaper", "BlueFire", "GreenGlass", "WireFire", "GreenSnow"}, EMPTY);
++ setup("YellowPaper", EMPTY, EMPTY, EMPTY);
++ setup("WireGlass", new String[]{"YellowGrass"}, new String[]{"YellowGlass", "BigSpoon", "CopperSnow", "GreenGlass", "BlueEarth"}, EMPTY);
++ setup("BlueSpork", EMPTY, new String[]{"BrightBlueGrass"}, EMPTY);
++ setup("CopperShovel", EMPTY, new String[]{"GreenGlass"}, EMPTY);
++ setup("RedClam", EMPTY, EMPTY, EMPTY);
++ setup("EmeraldClam", EMPTY, new String[]{"BlueFire"}, EMPTY);
++ setup("DarkClam", EMPTY, new String[]{"GoldAir", "LightGlass"}, EMPTY);
++ setup("WireSpoon", EMPTY, new String[]{"GoldPaper", "LightSnow"}, EMPTY);
++ setup("CopperNeptune", EMPTY, new String[]{"GreenGlass", "BigGrass"}, EMPTY);
++ setup("RedNeptune", EMPTY, EMPTY, EMPTY);
++ setup("GreenAir", EMPTY, EMPTY, EMPTY);
++ setup("RedFire", new String[]{"BrightBlueGrass", "BigPaper"}, new String[]{"BlueFire", "GreenGlass", "BigGrass"}, EMPTY);
++ }
++
++ @BeforeEach
++ public void loadProviders() {
++ AtomicInteger currentLoad = new AtomicInteger();
++ ModernPluginLoadingStrategy<PaperTestPlugin> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++ @Override
++ public void applyContext(PluginProvider<PaperTestPlugin> provider, DependencyContext dependencyContext) {
++ }
++
++ @Override
++ public boolean load(PluginProvider<PaperTestPlugin> provider, PaperTestPlugin provided) {
++ LOAD_ORDER.put(provider.getMeta().getName(), currentLoad.getAndIncrement());
++ return false;
++ }
++
++ });
++
++ modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS, new SimpleMetaDependencyTree(GraphBuilder.directed().build()));
++ }
++
++ @Test
++ public void testDependencies() {
++ for (PluginProvider<PaperTestPlugin> provider : REGISTERED_PROVIDERS) {
++ TestPluginMeta pluginMeta = (TestPluginMeta) provider.getMeta();
++ String identifier = pluginMeta.getName();
++ Assertions.assertTrue(LOAD_ORDER.containsKey(identifier), "Provider wasn't loaded! (%s)".formatted(identifier));
++
++ int index = LOAD_ORDER.get(identifier);
++
++ // Hard dependencies should be loaded BEFORE
++ for (String hardDependency : pluginMeta.getPluginDependencies()) {
++ Assertions.assertTrue(LOAD_ORDER.containsKey(hardDependency), "Plugin (%s) is missing hard dependency (%s)".formatted(identifier, hardDependency));
++
++ int dependencyIndex = LOAD_ORDER.get(hardDependency);
++ Assertions.assertTrue(index > dependencyIndex, "Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, hardDependency));
++ }
++
++ for (String softDependency : pluginMeta.getPluginSoftDependencies()) {
++ if (!LOAD_ORDER.containsKey(softDependency)) {
++ continue;
++ }
++
++ int dependencyIndex = LOAD_ORDER.get(softDependency);
++
++ Assertions.assertTrue(index > dependencyIndex, "Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, softDependency));
++ }
++
++ for (String loadBefore : pluginMeta.getLoadBeforePlugins()) {
++ if (!LOAD_ORDER.containsKey(loadBefore)) {
++ continue;
++ }
++
++ int dependencyIndex = LOAD_ORDER.get(loadBefore);
++ Assertions.assertTrue(index < dependencyIndex, "Plugin (%s) was NOT loaded BEFORE loadbefore dependency. (%s)".formatted(identifier, loadBefore));
++ }
++ }
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3052b32905c8102357b7b3a5ed2df103fea83ab8
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
+@@ -0,0 +1,78 @@
++package io.papermc.paper.plugin;
++
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.support.environment.Normal;
++import org.junit.jupiter.api.AfterEach;
++import org.junit.jupiter.api.Test;
++
++import static org.hamcrest.MatcherAssert.assertThat;
++import static org.hamcrest.Matchers.empty;
++import static org.hamcrest.Matchers.is;
++import static org.hamcrest.Matchers.nullValue;
++
++@Normal
++public class PluginManagerTest {
++
++ private static final PluginManager pm = Bukkit.getPluginManager();
++
++ @Test
++ public void testSyncSameThread() {
++ final Event event = new TestEvent(false);
++ pm.callEvent(event);
++ }
++
++ @Test
++ public void testRemovePermissionByNameLower() {
++ this.testRemovePermissionByName("lower");
++ }
++
++ @Test
++ public void testRemovePermissionByNameUpper() {
++ this.testRemovePermissionByName("UPPER");
++ }
++
++ @Test
++ public void testRemovePermissionByNameCamel() {
++ this.testRemovePermissionByName("CaMeL");
++ }
++
++ @Test
++ public void testRemovePermissionByPermissionLower() {
++ this.testRemovePermissionByPermission("lower");
++ }
++
++ @Test
++ public void testRemovePermissionByPermissionUpper() {
++ this.testRemovePermissionByPermission("UPPER");
++ }
++
++ @Test
++ public void testRemovePermissionByPermissionCamel() {
++ this.testRemovePermissionByPermission("CaMeL");
++ }
++
++ private void testRemovePermissionByName(final String name) {
++ final Permission perm = new Permission(name);
++ pm.addPermission(perm);
++ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
++ pm.removePermission(name);
++ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
++ }
++
++ private void testRemovePermissionByPermission(final String name) {
++ final Permission perm = new Permission(name);
++ pm.addPermission(perm);
++ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
++ pm.removePermission(perm);
++ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
++ }
++
++ @AfterEach
++ public void tearDown() {
++ pm.clearPlugins();
++ assertThat(pm.getPermissions(), is(empty()));
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java b/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9f4486f736bad41a6c8c9c86e57226a45ae29a61
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginNamingTest.java
+@@ -0,0 +1,30 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import org.bukkit.support.environment.Normal;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.Test;
++
++@Normal
++public class PluginNamingTest {
++ private static final String TEST_NAME = "Test_Plugin";
++ private static final String TEST_VERSION = "1.0";
++
++ private final PaperPluginMeta pluginMeta;
++
++ public PluginNamingTest() {
++ this.pluginMeta = new PaperPluginMeta();
++ this.pluginMeta.setName(TEST_NAME);
++ this.pluginMeta.setVersion(TEST_VERSION);
++ }
++
++ @Test
++ public void testName() {
++ Assertions.assertEquals(TEST_NAME, this.pluginMeta.getName());
++ }
++
++ @Test
++ public void testDisplayName() {
++ Assertions.assertEquals(TEST_NAME + " v" + TEST_VERSION, this.pluginMeta.getDisplayName());
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0b7f1521f8bf4b18dfdf9403862b5dc6b394a4d9
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventHandler;
++import org.bukkit.event.Listener;
++import org.bukkit.support.environment.Normal;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.Test;
++
++@Normal
++public class SyntheticEventTest {
++
++ @Test
++ public void test() {
++ PaperTestPlugin paperTestPlugin = new PaperTestPlugin("synthetictest");
++ PaperPluginManagerImpl paperPluginManager = new PaperPluginManagerImpl(Bukkit.getServer(), null, null);
++
++ TestEvent event = new TestEvent(false);
++ Impl impl = new Impl();
++
++ paperPluginManager.registerEvents(impl, paperTestPlugin);
++ paperPluginManager.callEvent(event);
++
++ Assertions.assertEquals(1, impl.callCount);
++ }
++
++ public abstract static class Base<E extends Event> implements Listener {
++ int callCount = 0;
++
++ public void accept(E evt) {
++ callCount++;
++ }
++ }
++
++ public static class Impl extends Base<TestEvent> {
++ @Override
++ @EventHandler
++ public void accept(TestEvent evt) {
++ super.accept(evt);
++ }
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestEvent.java b/src/test/java/io/papermc/paper/plugin/TestEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..04903794a8ee4dd73162ae240862ff6dc4cb4e24
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestEvent.java
+@@ -0,0 +1,22 @@
++package io.papermc.paper.plugin;
++
++
++import org.bukkit.event.Event;
++import org.bukkit.event.HandlerList;
++
++public class TestEvent extends Event {
++ private static final HandlerList handlers = new HandlerList();
++
++ public TestEvent(boolean async) {
++ super(async);
++ }
++
++ @Override
++ public HandlerList getHandlers() {
++ return handlers;
++ }
++
++ public static HandlerList getHandlerList() {
++ return handlers;
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..349932b582349c988f3244552b6e6da5dede3d1d
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
+@@ -0,0 +1,83 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++import java.util.jar.JarFile;
++
++public class TestJavaPluginProvider implements PluginProvider<PaperTestPlugin> {
++
++ private final TestPluginMeta testPluginConfiguration;
++
++ public TestJavaPluginProvider(TestPluginMeta testPluginConfiguration) {
++ this.testPluginConfiguration = testPluginConfiguration;
++ }
++
++ @Override
++ public @NotNull Path getSource() {
++ return Path.of("dummy");
++ }
++
++ @Override
++ public JarFile file() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public PaperTestPlugin createInstance() {
++ return new PaperTestPlugin(this.testPluginConfiguration);
++ }
++
++ @Override
++ public TestPluginMeta getMeta() {
++ return this.testPluginConfiguration;
++ }
++
++ @Override
++ public ComponentLogger getLogger() {
++ return ComponentLogger.logger("TestPlugin");
++ }
++
++ @Override
++ public LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad) {
++ return new LoadOrderConfiguration() {
++ @Override
++ public @NotNull List<String> getLoadBefore() {
++ return TestJavaPluginProvider.this.testPluginConfiguration.getLoadBeforePlugins();
++ }
++
++ @Override
++ public @NotNull List<String> getLoadAfter() {
++ List<String> loadAfter = new ArrayList<>();
++ loadAfter.addAll(TestJavaPluginProvider.this.testPluginConfiguration.getPluginDependencies());
++ loadAfter.addAll(TestJavaPluginProvider.this.testPluginConfiguration.getPluginSoftDependencies());
++ return loadAfter;
++ }
++
++ @Override
++ public @NotNull PluginMeta getMeta() {
++ return TestJavaPluginProvider.this.testPluginConfiguration;
++ }
++ };
++ }
++
++ @Override
++ public List<String> validateDependencies(@NotNull DependencyContext context) {
++ List<String> missingDependencies = new ArrayList<>();
++ for (String hardDependency : this.getMeta().getPluginDependencies()) {
++ if (!context.hasDependency(hardDependency)) {
++ missingDependencies.add(hardDependency);
++ }
++ }
++
++ return missingDependencies;
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ba271c35eb2804f94cfc893bf94affb9ae13d3ba
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
+@@ -0,0 +1,114 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginLoadOrder;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++
++public class TestPluginMeta implements PluginMeta {
++
++ private final String identifier;
++ private List<String> hardDependencies = List.of();
++ private List<String> softDependencies = List.of();
++ private List<String> loadBefore = List.of();
++
++ public TestPluginMeta(String identifier) {
++ this.identifier = identifier;
++ }
++
++ @Override
++ public @NotNull String getName() {
++ return this.identifier;
++ }
++
++ @Override
++ public @NotNull String getMainClass() {
++ return "null";
++ }
++
++ @Override
++ public @NotNull PluginLoadOrder getLoadOrder() {
++ return PluginLoadOrder.POSTWORLD;
++ }
++
++ @Override
++ public @NotNull String getVersion() {
++ return "1.0";
++ }
++
++ @Override
++ public @Nullable String getLoggerPrefix() {
++ return this.identifier;
++ }
++
++ public void setHardDependencies(List<String> hardDependencies) {
++ this.hardDependencies = hardDependencies;
++ }
++
++ @Override
++ public @NotNull List<String> getPluginDependencies() {
++ return this.hardDependencies;
++ }
++
++ public void setSoftDependencies(List<String> softDependencies) {
++ this.softDependencies = softDependencies;
++ }
++
++ @Override
++ public @NotNull List<String> getPluginSoftDependencies() {
++ return this.softDependencies;
++ }
++
++ public void setLoadBefore(List<String> loadBefore) {
++ this.loadBefore = loadBefore;
++ }
++
++ @Override
++ public @NotNull List<String> getLoadBeforePlugins() {
++ return this.loadBefore;
++ }
++
++ @Override
++ public @NotNull List<String> getProvidedPlugins() {
++ return List.of();
++ }
++
++ @Override
++ public @NotNull List<String> getAuthors() {
++ return List.of();
++ }
++
++ @Override
++ public @NotNull List<String> getContributors() {
++ return List.of();
++ }
++
++ @Override
++ public @Nullable String getDescription() {
++ return "null";
++ }
++
++ @Override
++ public @Nullable String getWebsite() {
++ return "null";
++ }
++
++ @Override
++ public @NotNull List<Permission> getPermissions() {
++ return List.of();
++ }
++
++ @Override
++ public @NotNull PermissionDefault getPermissionDefault() {
++ return PermissionDefault.TRUE;
++ }
++
++ @Override
++ public @NotNull String getAPIVersion() {
++ return "null";
++ }
++}
+diff --git a/src/test/java/org/bukkit/support/DummyServerHelper.java b/src/test/java/org/bukkit/support/DummyServerHelper.java
+index bdfa164ea21cba91b30b965d65d47112111a1209..2fed099bc91a8591a2415493b333f9c18bfe35f6 100644
+--- a/src/test/java/org/bukkit/support/DummyServerHelper.java
++++ b/src/test/java/org/bukkit/support/DummyServerHelper.java
+@@ -87,7 +87,7 @@ public final class DummyServerHelper {
+ // Paper start - testing additions
+ final Thread currentThread = Thread.currentThread();
+ when(instance.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread().equals(currentThread));
+- final org.bukkit.plugin.PluginManager pluginManager = new org.bukkit.plugin.SimplePluginManager(instance, new org.bukkit.command.SimpleCommandMap(instance));
++ final org.bukkit.plugin.PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(instance, new org.bukkit.command.SimpleCommandMap(instance), null);
+ when(instance.getPluginManager()).thenReturn(pluginManager);
+ // Paper end - testing additions
+
diff --git a/patches/server/0020-Plugin-remapping.patch b/patches/server/0020-Plugin-remapping.patch
new file mode 100644
index 0000000000..7dd0c11a62
--- /dev/null
+++ b/patches/server/0020-Plugin-remapping.patch
@@ -0,0 +1,1917 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Sat, 29 Oct 2022 15:22:32 -0700
+Subject: [PATCH] Plugin remapping
+
+Co-authored-by: Nassim Jahnke <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 19d9cbcaa05061a5bedf5b1d821138091acfe973..884ac16677ee3f52174c7bbf7b34896bcbf04bbc 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -61,6 +61,7 @@ dependencies {
+ testImplementation("org.ow2.asm:asm-tree:9.7.1")
+ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
+ implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
++ implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins
+ }
+
+ paperweight {
+@@ -188,20 +189,41 @@ val runtimeClasspathWithoutVanillaServer = configurations.runtimeClasspath.flatM
+ runtime.filterNot { it.asFile.absolutePath == vanilla }
+ }
+
+-tasks.registerRunTask("runServerJar") {
+- description = "Spin up a test server from the serverJar archiveFile"
+- classpath(tasks.serverJar.flatMap { it.archiveFile })
++tasks.registerRunTask("runServer") {
++ description = "Spin up a test server from the Mojang mapped server jar"
++ classpath(tasks.includeMappings.flatMap { it.outputJar })
+ classpath(runtimeClasspathWithoutVanillaServer)
+ }
+
+-tasks.registerRunTask("runReobf") {
++tasks.registerRunTask("runReobfServer") {
+ description = "Spin up a test server from the reobfJar output jar"
+ classpath(tasks.reobfJar.flatMap { it.outputJar })
+ classpath(runtimeClasspathWithoutVanillaServer)
+ }
+
+-tasks.registerRunTask("runDev") {
+- description = "Spin up a non-relocated Mojang-mapped test server"
++tasks.registerRunTask("runDevServer") {
++ description = "Spin up a test server without assembling a jar"
+ classpath(sourceSets.main.map { it.runtimeClasspath })
+ jvmArgs("-DPaper.pushPaperAssetsRoot=true")
+ }
++
++tasks.registerRunTask("runBundler") {
++ description = "Spin up a test server from the Mojang mapped bundler jar"
++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createMojmapBundlerJar").flatMap { it.outputZip })
++ mainClass.set(null as String?)
++}
++tasks.registerRunTask("runReobfBundler") {
++ description = "Spin up a test server from the reobf bundler jar"
++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createReobfBundlerJar").flatMap { it.outputZip })
++ mainClass.set(null as String?)
++}
++tasks.registerRunTask("runPaperclip") {
++ description = "Spin up a test server from the Mojang mapped Paperclip jar"
++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createMojmapPaperclipJar").flatMap { it.outputZip })
++ mainClass.set(null as String?)
++}
++tasks.registerRunTask("runReobfPaperclip") {
++ description = "Spin up a test server from the reobf Paperclip jar"
++ classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createReobfPaperclipJar").flatMap { it.outputZip })
++ mainClass.set(null as String?)
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+index 708e5bb9bbf0476fcc2c4b92c6830b094703b43e..49d8e207795997e5deaf830eb971067f84bfc791 100644
+--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+@@ -6,10 +6,12 @@ import io.papermc.paper.plugin.entrypoint.Entrypoint;
+ import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+ import io.papermc.paper.plugin.provider.PluginProvider;
+ import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.pluginremap.PluginRemapper;
++import java.util.function.Function;
+ import joptsimple.OptionSet;
+ import net.minecraft.server.dedicated.DedicatedServer;
+ import org.bukkit.configuration.file.YamlConfiguration;
+-import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.plugin.java.LibraryLoader;
+ import org.jetbrains.annotations.NotNull;
+ import org.jetbrains.annotations.Nullable;
+ import org.slf4j.Logger;
+@@ -25,10 +27,15 @@ public class PluginInitializerManager {
+ private static PluginInitializerManager impl;
+ private final Path pluginDirectory;
+ private final Path updateDirectory;
++ public final io.papermc.paper.pluginremap.@org.checkerframework.checker.nullness.qual.MonotonicNonNull PluginRemapper pluginRemapper; // Paper
+
+ PluginInitializerManager(final Path pluginDirectory, final Path updateDirectory) {
+ this.pluginDirectory = pluginDirectory;
+ this.updateDirectory = updateDirectory;
++ this.pluginRemapper = Boolean.getBoolean("paper.disablePluginRemapping")
++ ? null
++ : PluginRemapper.create(pluginDirectory);
++ LibraryLoader.REMAPPER = this.pluginRemapper == null ? Function.identity() : this.pluginRemapper::remapLibraries;
+ }
+
+ private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception {
+@@ -96,6 +103,7 @@ public class PluginInitializerManager {
+ public static void load(OptionSet optionSet) throws Exception {
+ // We have to load the bukkit configuration inorder to get the update folder location.
+ io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionSet);
++ if (pluginSystem.pluginRemapper != null) pluginSystem.pluginRemapper.loadingPlugins();
+
+ // Register the default plugin directory
+ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath());
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+index f38ecd7f65dc24e4a3f0bc675e3730287ac353f1..f576060c8fe872772bbafe2016fc9b83a3c095f1 100644
+--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -1,5 +1,6 @@
+ package io.papermc.paper.plugin.loader;
+
++import io.papermc.paper.plugin.PluginInitializerManager;
+ import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+ import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
+ import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
+@@ -45,9 +46,12 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+ }
+
+ List<Path> paths = paperLibraryStore.getPaths();
++ if (PluginInitializerManager.instance().pluginRemapper != null) {
++ paths = PluginInitializerManager.instance().pluginRemapper.remapLibraries(paths);
++ }
+ URL[] urls = new URL[paths.size()];
+ for (int i = 0; i < paths.size(); i++) {
+- Path path = paperLibraryStore.getPaths().get(i);
++ Path path = paths.get(i);
+ try {
+ urls[i] = path.toUri().toURL();
+ } catch (MalformedURLException e) {
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+index 226f457db6c1461c943c157b2b91e7450abc9dc6..0846d3a904e470ae1920c5c8be3df9c5dfc3de27 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+@@ -17,7 +17,7 @@ import org.slf4j.Logger;
+ public class DirectoryProviderSource implements ProviderSource<Path, List<Path>> {
+
+ public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource();
+- private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted);
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("Directory '%s'"::formatted, false); // Paper - Remap plugins
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ @Override
+@@ -37,6 +37,11 @@ public class DirectoryProviderSource implements ProviderSource<Path, List<Path>>
+ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e);
+ }
+ });
++ // Paper start - Remap plugins
++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) {
++ return io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewritePluginDirectory(files);
++ }
++ // Paper end - Remap plugins
+ return files;
+ }
+
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+index 5b58df8df7efca0f67e3a14dd71051dfd7a26079..6b8ed8a0baaf4a57d20e57cec3400af5561ddd79 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+@@ -24,9 +24,15 @@ import java.util.jar.JarFile;
+ public class FileProviderSource implements ProviderSource<Path, Path> {
+
+ private final Function<Path, String> contextChecker;
++ private final boolean applyRemap;
+
+- public FileProviderSource(Function<Path, String> contextChecker) {
++ public FileProviderSource(Function<Path, String> contextChecker, boolean applyRemap) {
+ this.contextChecker = contextChecker;
++ this.applyRemap = applyRemap;
++ }
++
++ public FileProviderSource(Function<Path, String> contextChecker) {
++ this(contextChecker, true);
+ }
+
+ @Override
+@@ -50,6 +56,11 @@ public class FileProviderSource implements ProviderSource<Path, Path> {
+ } catch (Exception exception) {
+ throw new RuntimeException(source + " failed to update!", exception);
+ }
++ // Paper start - Remap plugins
++ if (this.applyRemap && io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) {
++ context = io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewritePlugin(context);
++ }
++ // Paper end - Remap plugins
+ return context;
+ }
+
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+index ac55ae0e30119556f01e2e36c20fc63a111fae5f..c2b60c74513544e5d96110c7c3ff80e8f1b686d1 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+@@ -14,7 +14,7 @@ import java.util.List;
+ public class PluginFlagProviderSource implements ProviderSource<List<Path>, List<Path>> {
+
+ public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource();
+- private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted);
++ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted, false);
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ @Override
+@@ -27,6 +27,11 @@ public class PluginFlagProviderSource implements ProviderSource<List<Path>, List
+ LOGGER.error("Error preparing plugin context: " + e.getMessage(), e);
+ }
+ }
++ // Paper start - Remap plugins
++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null && !files.isEmpty()) {
++ return io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.rewriteExtraPlugins(files);
++ }
++ // Paper end - Remap plugins
+ return files;
+ }
+
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+index 87128685015d550440a798028f50be24bc755f6c..8d0da6e46d4eb5eb05c3144510c4ef083559d0ec 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+@@ -22,9 +22,10 @@ import java.util.jar.JarFile;
+ */
+ public abstract class PluginFileType<T, C extends PluginMeta> {
+
++ public static final String PAPER_PLUGIN_YML = "paper-plugin.yml";
+ private static final List<String> CONFIG_TYPES = new ArrayList<>();
+
+- public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) {
++ public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>(PAPER_PLUGIN_YML, PaperPluginParent.FACTORY) {
+ @Override
+ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) {
+ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null;
+diff --git a/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java b/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..99e658e3a0f08dbd90b3cf48609613e8a085fd64
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/DebugLogger.java
+@@ -0,0 +1,63 @@
++package io.papermc.paper.pluginremap;
++
++import java.io.IOException;
++import java.io.PrintWriter;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.function.Consumer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++/**
++ * {@link PrintWriter}-backed logger implementation for use with {@link net.neoforged.art.api.Renamer} which
++ * only opens the backing writer and logs messages when the {@link PluginRemapper#DEBUG_LOGGING} system property
++ * is set to true.
++ */
++@DefaultQualifier(NonNull.class)
++final class DebugLogger implements Consumer<String>, AutoCloseable {
++ private final @Nullable PrintWriter writer;
++
++ DebugLogger(final Path logFile) {
++ try {
++ this.writer = createWriter(logFile);
++ } catch (final IOException ex) {
++ throw new RuntimeException("Failed to initialize DebugLogger for file '" + logFile + "'", ex);
++ }
++ }
++
++ @Override
++ public void accept(final String line) {
++ this.useWriter(writer -> writer.println(line));
++ }
++
++ @Override
++ public void close() {
++ this.useWriter(PrintWriter::close);
++ }
++
++ private void useWriter(final Consumer<PrintWriter> op) {
++ final @Nullable PrintWriter writer = this.writer;
++ if (writer != null) {
++ op.accept(writer);
++ }
++ }
++
++ Consumer<String> debug() {
++ return line -> this.accept("[debug]: " + line);
++ }
++
++ static DebugLogger forOutputFile(final Path outputFile) {
++ return new DebugLogger(outputFile.resolveSibling(outputFile.getFileName() + ".log"));
++ }
++
++ private static @Nullable PrintWriter createWriter(final Path logFile) throws IOException {
++ if (!PluginRemapper.DEBUG_LOGGING) {
++ return null;
++ }
++ if (!Files.exists(logFile.getParent())) {
++ Files.createDirectories(logFile.getParent());
++ }
++ return new PrintWriter(logFile.toFile());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java b/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d738b31f0005aca352a511c1a57e76b627fca2dd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/InsertManifestAttribute.java
+@@ -0,0 +1,69 @@
++package io.papermc.paper.pluginremap;
++
++import java.io.ByteArrayInputStream;
++import java.io.ByteArrayOutputStream;
++import java.io.IOException;
++import java.util.Collection;
++import java.util.List;
++import java.util.Set;
++import java.util.jar.Attributes;
++import java.util.jar.Manifest;
++import net.neoforged.art.api.Transformer;
++
++final class InsertManifestAttribute implements Transformer {
++ static final String PAPERWEIGHT_NAMESPACE_MANIFEST_KEY = "paperweight-mappings-namespace";
++ static final String MOJANG_NAMESPACE = "mojang";
++ static final String MOJANG_PLUS_YARN_NAMESPACE = "mojang+yarn";
++ static final String SPIGOT_NAMESPACE = "spigot";
++ static final Set<String> KNOWN_NAMESPACES = Set.of(MOJANG_NAMESPACE, MOJANG_PLUS_YARN_NAMESPACE, SPIGOT_NAMESPACE);
++
++ private final String mainAttributesKey;
++ private final String namespace;
++ private final boolean createIfMissing;
++ private volatile boolean visitedManifest = false;
++
++ static Transformer addNamespaceManifestAttribute(final String namespace) {
++ return new InsertManifestAttribute(PAPERWEIGHT_NAMESPACE_MANIFEST_KEY, namespace, true);
++ }
++
++ InsertManifestAttribute(
++ final String mainAttributesKey,
++ final String namespace,
++ final boolean createIfMissing
++ ) {
++ this.mainAttributesKey = mainAttributesKey;
++ this.namespace = namespace;
++ this.createIfMissing = createIfMissing;
++ }
++
++ @Override
++ public ManifestEntry process(final ManifestEntry entry) {
++ this.visitedManifest = true;
++ try {
++ final Manifest manifest = new Manifest(new ByteArrayInputStream(entry.getData()));
++ manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace);
++ final ByteArrayOutputStream out = new ByteArrayOutputStream();
++ manifest.write(out);
++ return ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray());
++ } catch (final IOException e) {
++ throw new RuntimeException("Failed to modify manifest", e);
++ }
++ }
++
++ @Override
++ public Collection<? extends Entry> getExtras() {
++ if (!this.visitedManifest && this.createIfMissing) {
++ final Manifest manifest = new Manifest();
++ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
++ manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace);
++ final ByteArrayOutputStream out = new ByteArrayOutputStream();
++ try {
++ manifest.write(out);
++ } catch (final IOException e) {
++ throw new RuntimeException("Failed to write manifest", e);
++ }
++ return List.of(ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray()));
++ }
++ return Transformer.super.getExtras();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..28857d0c9b53f2068d51b8f09ef40df7a2b97502
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java
+@@ -0,0 +1,438 @@
++package io.papermc.paper.pluginremap;
++
++import com.google.common.util.concurrent.ThreadFactoryBuilder;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.provider.type.PluginFileType;
++import io.papermc.paper.util.AtomicFiles;
++import io.papermc.paper.util.MappingEnvironment;
++import io.papermc.paper.util.concurrent.ScalingThreadPool;
++import java.io.BufferedInputStream;
++import java.io.IOException;
++import java.io.InputStream;
++import java.nio.file.FileSystem;
++import java.nio.file.FileSystems;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.CompletionException;
++import java.util.concurrent.Executor;
++import java.util.concurrent.ExecutorService;
++import java.util.concurrent.ThreadPoolExecutor;
++import java.util.concurrent.TimeUnit;
++import java.util.function.Predicate;
++import java.util.function.Supplier;
++import java.util.jar.Manifest;
++import java.util.stream.Stream;
++import net.minecraft.DefaultUncaughtExceptionHandlerWithName;
++import net.minecraft.util.ExceptionCollector;
++import net.neoforged.art.api.Renamer;
++import net.neoforged.art.api.SignatureStripperConfig;
++import net.neoforged.art.api.Transformer;
++import net.neoforged.srgutils.IMappingFile;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute;
++
++@DefaultQualifier(NonNull.class)
++public final class PluginRemapper {
++ public static final boolean DEBUG_LOGGING = Boolean.getBoolean("Paper.PluginRemapperDebug");
++ private static final String PAPER_REMAPPED = ".paper-remapped";
++ private static final String UNKNOWN_ORIGIN = "unknown-origin";
++ private static final String LIBRARIES = "libraries";
++ private static final String EXTRA_PLUGINS = "extra-plugins";
++ private static final String REMAP_CLASSPATH = "remap-classpath";
++ private static final String REVERSED_MAPPINGS = "mappings/reversed";
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final ExecutorService threadPool;
++ private final ReobfServer reobf;
++ private final RemappedPluginIndex remappedPlugins;
++ private final RemappedPluginIndex extraPlugins;
++ private final UnknownOriginRemappedPluginIndex unknownOrigin;
++ private final UnknownOriginRemappedPluginIndex libraries;
++ private @Nullable CompletableFuture<IMappingFile> reversedMappings;
++
++ public PluginRemapper(final Path pluginsDir) {
++ this.threadPool = createThreadPool();
++ final CompletableFuture<IMappingFile> mappings = CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool);
++ final Path remappedPlugins = pluginsDir.resolve(PAPER_REMAPPED);
++ this.reversedMappings = this.reversedMappingsFuture(() -> mappings, remappedPlugins, this.threadPool);
++ this.reobf = new ReobfServer(remappedPlugins.resolve(REMAP_CLASSPATH), mappings, this.threadPool);
++ this.remappedPlugins = new RemappedPluginIndex(remappedPlugins, false);
++ this.extraPlugins = new RemappedPluginIndex(this.remappedPlugins.dir().resolve(EXTRA_PLUGINS), true);
++ this.unknownOrigin = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(UNKNOWN_ORIGIN));
++ this.libraries = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(LIBRARIES));
++ }
++
++ public static @Nullable PluginRemapper create(final Path pluginsDir) {
++ if (MappingEnvironment.reobf() || !MappingEnvironment.hasMappings()) {
++ return null;
++ }
++
++ return new PluginRemapper(pluginsDir);
++ }
++
++ public void shutdown() {
++ this.threadPool.shutdown();
++ this.save(true);
++ boolean didShutdown;
++ try {
++ didShutdown = this.threadPool.awaitTermination(3L, TimeUnit.SECONDS);
++ } catch (final InterruptedException ex) {
++ didShutdown = false;
++ }
++ if (!didShutdown) {
++ this.threadPool.shutdownNow();
++ }
++ }
++
++ public void save(final boolean clean) {
++ this.remappedPlugins.write();
++ this.extraPlugins.write();
++ this.unknownOrigin.write(clean);
++ this.libraries.write(clean);
++ }
++
++ // Called on startup and reload
++ public void loadingPlugins() {
++ if (this.reversedMappings == null) {
++ this.reversedMappings = this.reversedMappingsFuture(
++ () -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool),
++ this.remappedPlugins.dir(),
++ this.threadPool
++ );
++ }
++ }
++
++ // Called after all plugins enabled during startup/reload
++ public void pluginsEnabled() {
++ this.reversedMappings = null;
++ this.save(false);
++ }
++
++ public List<Path> remapLibraries(final List<Path> libraries) {
++ final List<CompletableFuture<Path>> tasks = new ArrayList<>();
++ for (final Path lib : libraries) {
++ if (!lib.getFileName().toString().endsWith(".jar")) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Library '{}' is not a jar.", lib);
++ }
++ tasks.add(CompletableFuture.completedFuture(lib));
++ continue;
++ }
++ final @Nullable Path cached = this.libraries.getIfPresent(lib);
++ if (cached != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Library '{}' has not changed since last remap.", lib);
++ }
++ tasks.add(CompletableFuture.completedFuture(cached));
++ continue;
++ }
++ tasks.add(this.remapLibrary(this.libraries, lib));
++ }
++ return waitForAll(tasks);
++ }
++
++ public Path rewritePlugin(final Path plugin) {
++ // Already remapped
++ if (plugin.getParent().equals(this.remappedPlugins.dir())
++ || plugin.getParent().equals(this.extraPlugins.dir())) {
++ return plugin;
++ }
++
++ final @Nullable Path cached = this.unknownOrigin.getIfPresent(plugin);
++ if (cached != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Plugin '{}' has not changed since last remap.", plugin);
++ }
++ return cached;
++ }
++
++ return this.remapPlugin(this.unknownOrigin, plugin).join();
++ }
++
++ public List<Path> rewriteExtraPlugins(final List<Path> plugins) {
++ final @Nullable List<Path> allCached = this.extraPlugins.getAllIfPresent(plugins);
++ if (allCached != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("All extra plugins have a remapped variant cached.");
++ }
++ return allCached;
++ }
++
++ final List<CompletableFuture<Path>> tasks = new ArrayList<>();
++ for (final Path file : plugins) {
++ final @Nullable Path cached = this.extraPlugins.getIfPresent(file);
++ if (cached != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Extra plugin '{}' has not changed since last remap.", file);
++ }
++ tasks.add(CompletableFuture.completedFuture(cached));
++ continue;
++ }
++ tasks.add(this.remapPlugin(this.extraPlugins, file));
++ }
++ return waitForAll(tasks);
++ }
++
++ public List<Path> rewritePluginDirectory(final List<Path> jars) {
++ final @Nullable List<Path> remappedJars = this.remappedPlugins.getAllIfPresent(jars);
++ if (remappedJars != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("All plugins have a remapped variant cached.");
++ }
++ return remappedJars;
++ }
++
++ final List<CompletableFuture<Path>> tasks = new ArrayList<>();
++ for (final Path file : jars) {
++ final @Nullable Path existingFile = this.remappedPlugins.getIfPresent(file);
++ if (existingFile != null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Plugin '{}' has not changed since last remap.", file);
++ }
++ tasks.add(CompletableFuture.completedFuture(existingFile));
++ continue;
++ }
++
++ tasks.add(this.remapPlugin(this.remappedPlugins, file));
++ }
++ return waitForAll(tasks);
++ }
++
++ private static IMappingFile reverse(final IMappingFile mappings) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Reversing mappings...");
++ }
++ final long start = System.currentTimeMillis();
++ final IMappingFile reversed = mappings.reverse();
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Done reversing mappings in {}ms.", System.currentTimeMillis() - start);
++ }
++ return reversed;
++ }
++
++ private CompletableFuture<IMappingFile> reversedMappingsFuture(
++ final Supplier<CompletableFuture<IMappingFile>> mappingsFuture,
++ final Path remappedPlugins,
++ final Executor executor
++ ) {
++ return CompletableFuture.supplyAsync(() -> {
++ try {
++ final String mappingsHash = MappingEnvironment.mappingsHash();
++ final String fName = mappingsHash + ".tiny";
++ final Path reversedMappings1 = remappedPlugins.resolve(REVERSED_MAPPINGS);
++ final Path file = reversedMappings1.resolve(fName);
++ if (Files.isDirectory(reversedMappings1)) {
++ if (Files.isRegularFile(file)) {
++ return CompletableFuture.completedFuture(
++ loadMappings("Reversed", Files.newInputStream(file))
++ );
++ } else {
++ for (final Path oldFile : list(reversedMappings1, Files::isRegularFile)) {
++ Files.delete(oldFile);
++ }
++ }
++ } else {
++ Files.createDirectories(reversedMappings1);
++ }
++ return mappingsFuture.get().thenApply(loadedMappings -> {
++ final IMappingFile reversed = reverse(loadedMappings);
++ try {
++ AtomicFiles.atomicWrite(file, writeTo -> {
++ reversed.write(writeTo, IMappingFile.Format.TINY, false);
++ });
++ } catch (final IOException e) {
++ throw new RuntimeException("Failed to write reversed mappings", e);
++ }
++ return reversed;
++ });
++ } catch (final IOException e) {
++ throw new RuntimeException("Failed to load reversed mappings", e);
++ }
++ }, executor).thenCompose(f -> f);
++ }
++
++ private CompletableFuture<Path> remapPlugin(
++ final RemappedPluginIndex index,
++ final Path inputFile
++ ) {
++ return this.remap(index, inputFile, false);
++ }
++
++ private CompletableFuture<Path> remapLibrary(
++ final RemappedPluginIndex index,
++ final Path inputFile
++ ) {
++ return this.remap(index, inputFile, true);
++ }
++
++ /**
++ * Returns the remapped file if remapping was necessary, otherwise null.
++ *
++ * @param index remapped plugin index
++ * @param inputFile input file
++ * @return remapped file, or inputFile if no remapping was necessary
++ */
++ private CompletableFuture<Path> remap(
++ final RemappedPluginIndex index,
++ final Path inputFile,
++ final boolean library
++ ) {
++ final Path destination = index.input(inputFile);
++
++ try (final FileSystem fs = FileSystems.newFileSystem(inputFile, new HashMap<>())) {
++ // Leave dummy files if no remapping is required, so that we can check if they exist without copying the whole file
++ final Path manifestPath = fs.getPath("META-INF/MANIFEST.MF");
++ final @Nullable String ns;
++ if (Files.exists(manifestPath)) {
++ final Manifest manifest;
++ try (final InputStream in = new BufferedInputStream(Files.newInputStream(manifestPath))) {
++ manifest = new Manifest(in);
++ }
++ ns = manifest.getMainAttributes().getValue(InsertManifestAttribute.PAPERWEIGHT_NAMESPACE_MANIFEST_KEY);
++ } else {
++ ns = null;
++ }
++
++ if (ns != null && !InsertManifestAttribute.KNOWN_NAMESPACES.contains(ns)) {
++ throw new RuntimeException("Failed to remap plugin " + inputFile + " with unknown mapping namespace '" + ns + "'");
++ }
++
++ final boolean mojangMappedManifest = ns != null && (ns.equals(InsertManifestAttribute.MOJANG_NAMESPACE) || ns.equals(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE));
++ if (library) {
++ if (mojangMappedManifest) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Library '{}' is already Mojang mapped.", inputFile);
++ }
++ index.skip(inputFile);
++ return CompletableFuture.completedFuture(inputFile);
++ } else if (ns == null) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Library '{}' does not specify a mappings namespace (not remapping).", inputFile);
++ }
++ index.skip(inputFile);
++ return CompletableFuture.completedFuture(inputFile);
++ }
++ } else {
++ if (mojangMappedManifest) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Plugin '{}' is already Mojang mapped.", inputFile);
++ }
++ index.skip(inputFile);
++ return CompletableFuture.completedFuture(inputFile);
++ } else if (ns == null && Files.exists(fs.getPath(PluginFileType.PAPER_PLUGIN_YML))) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Plugin '{}' is a Paper plugin with no namespace specified.", inputFile);
++ }
++ index.skip(inputFile);
++ return CompletableFuture.completedFuture(inputFile);
++ }
++ }
++ } catch (final IOException ex) {
++ return CompletableFuture.failedFuture(new RuntimeException("Failed to open plugin jar " + inputFile, ex));
++ }
++
++ return this.reobf.remapped().thenApplyAsync(reobfServer -> {
++ LOGGER.info("Remapping {} '{}'...", library ? "library" : "plugin", inputFile);
++ final long start = System.currentTimeMillis();
++ try (final DebugLogger logger = DebugLogger.forOutputFile(destination)) {
++ try (final Renamer renamer = Renamer.builder()
++ .add(Transformer.renamerFactory(this.mappings(), false))
++ .add(addNamespaceManifestAttribute(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE))
++ .add(Transformer.signatureStripperFactory(SignatureStripperConfig.ALL))
++ .lib(reobfServer.toFile())
++ .threads(1)
++ .logger(logger)
++ .debug(logger.debug())
++ .build()) {
++ renamer.run(inputFile.toFile(), destination.toFile());
++ }
++ } catch (final Exception ex) {
++ throw new RuntimeException("Failed to remap plugin jar '" + inputFile + "'", ex);
++ }
++ LOGGER.info("Done remapping {} '{}' in {}ms.", library ? "library" : "plugin", inputFile, System.currentTimeMillis() - start);
++ return destination;
++ }, this.threadPool);
++ }
++
++ private IMappingFile mappings() {
++ final @Nullable CompletableFuture<IMappingFile> mappings = this.reversedMappings;
++ if (mappings == null) {
++ return this.reversedMappingsFuture(
++ () -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, Runnable::run),
++ this.remappedPlugins.dir(),
++ Runnable::run
++ ).join();
++ }
++ return mappings.join();
++ }
++
++ private static IMappingFile loadReobfMappings() {
++ return loadMappings("Reobf", MappingEnvironment.mappingsStream());
++ }
++
++ private static IMappingFile loadMappings(final String name, final InputStream stream) {
++ try (stream) {
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Loading {} mappings...", name);
++ }
++ final long start = System.currentTimeMillis();
++ final IMappingFile load = IMappingFile.load(stream);
++ if (DEBUG_LOGGING) {
++ LOGGER.info("Done loading {} mappings in {}ms.", name, System.currentTimeMillis() - start);
++ }
++ return load;
++ } catch (final IOException ex) {
++ throw new RuntimeException("Failed to load " + name + " mappings", ex);
++ }
++ }
++
++ static List<Path> list(final Path dir, final Predicate<Path> filter) {
++ try (final Stream<Path> stream = Files.list(dir)) {
++ return stream.filter(filter).toList();
++ } catch (final IOException ex) {
++ throw new RuntimeException("Failed to list directory '" + dir + "'", ex);
++ }
++ }
++
++ private static List<Path> waitForAll(final List<CompletableFuture<Path>> tasks) {
++ final ExceptionCollector<Exception> collector = new ExceptionCollector<>();
++ final List<Path> ret = new ArrayList<>();
++ for (final CompletableFuture<Path> task : tasks) {
++ try {
++ ret.add(task.join());
++ } catch (final CompletionException ex) {
++ collector.add(ex);
++ }
++ }
++ try {
++ collector.throwIfPresent();
++ } catch (final Exception ex) {
++ // Don't hard fail during bootstrap/plugin loading. The plugin(s) in question will be skipped
++ LOGGER.error("Encountered exception remapping plugins", ex);
++ }
++ return ret;
++ }
++
++ private static ThreadPoolExecutor createThreadPool() {
++ return new ThreadPoolExecutor(
++ 0,
++ 4,
++ 5L,
++ TimeUnit.SECONDS,
++ ScalingThreadPool.createUnboundedQueue(),
++ new ThreadFactoryBuilder()
++ .setNameFormat("Paper Plugin Remapper Thread - %1$d")
++ .setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(LOGGER))
++ .build(),
++ ScalingThreadPool.defaultReEnqueuePolicy()
++ );
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java b/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..86fc60452404d1f4609c25a90c4803ffb80dc8ab
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/RemappedPluginIndex.java
+@@ -0,0 +1,212 @@
++package io.papermc.paper.pluginremap;
++
++import com.google.gson.Gson;
++import com.google.gson.GsonBuilder;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.Hashing;
++import io.papermc.paper.util.MappingEnvironment;
++import java.io.BufferedReader;
++import java.io.BufferedWriter;
++import java.io.IOException;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.function.Function;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++import org.spongepowered.configurate.loader.AtomicFiles;
++
++@DefaultQualifier(NonNull.class)
++class RemappedPluginIndex {
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final Gson GSON = new GsonBuilder()
++ .setPrettyPrinting()
++ .create();
++ private static final String INDEX_FILE_NAME = "index.json";
++
++ protected final State state;
++ private final Path dir;
++ private final Path indexFile;
++ private final boolean handleDuplicateFileNames;
++
++ // todo maybe hash remapped variants to ensure they haven't changed? probably unneeded
++ static final class State {
++ final Map<String, String> hashes = new HashMap<>();
++ final Set<String> skippedHashes = new HashSet<>();
++ private final String mappingsHash = MappingEnvironment.mappingsHash();
++ }
++
++ RemappedPluginIndex(final Path dir, final boolean handleDuplicateFileNames) {
++ this.dir = dir;
++ this.handleDuplicateFileNames = handleDuplicateFileNames;
++ if (!Files.exists(this.dir)) {
++ try {
++ Files.createDirectories(this.dir);
++ } catch (final IOException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
++
++ this.indexFile = dir.resolve(INDEX_FILE_NAME);
++ if (Files.isRegularFile(this.indexFile)) {
++ try {
++ this.state = this.readIndex();
++ } catch (final IOException e) {
++ throw new RuntimeException(e);
++ }
++ } else {
++ this.state = new State();
++ }
++ }
++
++ private State readIndex() throws IOException {
++ final State state;
++ try (final BufferedReader reader = Files.newBufferedReader(this.indexFile)) {
++ state = GSON.fromJson(reader, State.class);
++ }
++
++ // If mappings have changed, delete all cached files and create a new index
++ if (!state.mappingsHash.equals(MappingEnvironment.mappingsHash())) {
++ for (final String fileName : state.hashes.values()) {
++ Files.deleteIfExists(this.dir.resolve(fileName));
++ }
++ return new State();
++ }
++ return state;
++ }
++
++ Path dir() {
++ return this.dir;
++ }
++
++ /**
++ * Returns a list of cached paths if all of the input paths are present in the cache.
++ * The returned list may contain paths from different directories.
++ *
++ * @param paths plugin jar paths to check
++ * @return null if any of the paths are not present in the cache, otherwise a list of the cached paths
++ */
++ @Nullable List<Path> getAllIfPresent(final List<Path> paths) {
++ final Map<Path, String> hashCache = new HashMap<>();
++ final Function<Path, String> inputFileHash = path -> hashCache.computeIfAbsent(path, Hashing::sha256);
++
++ // Delete cached entries we no longer need
++ final Iterator<Map.Entry<String, String>> iterator = this.state.hashes.entrySet().iterator();
++ while (iterator.hasNext()) {
++ final Map.Entry<String, String> entry = iterator.next();
++ final String inputHash = entry.getKey();
++ final String fileName = entry.getValue();
++ if (paths.stream().anyMatch(path -> inputFileHash.apply(path).equals(inputHash))) {
++ // Hash is used, keep it
++ continue;
++ }
++
++ iterator.remove();
++ try {
++ Files.deleteIfExists(this.dir.resolve(fileName));
++ } catch (final IOException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
++
++ // Also clear hashes of skipped files
++ this.state.skippedHashes.removeIf(hash -> paths.stream().noneMatch(path -> inputFileHash.apply(path).equals(hash)));
++
++ final List<Path> ret = new ArrayList<>();
++ for (final Path path : paths) {
++ final String inputHash = inputFileHash.apply(path);
++ if (this.state.skippedHashes.contains(inputHash)) {
++ // Add the original path
++ ret.add(path);
++ continue;
++ }
++
++ final @Nullable Path cached = this.getIfPresent(inputHash);
++ if (cached == null) {
++ // Missing the remapped file
++ return null;
++ }
++ ret.add(cached);
++ }
++ return ret;
++ }
++
++ private String createCachedFileName(final Path in) {
++ if (this.handleDuplicateFileNames) {
++ final String fileName = in.getFileName().toString();
++ final int i = fileName.lastIndexOf(".jar");
++ return fileName.substring(0, i) + "-" + System.currentTimeMillis() + ".jar";
++ }
++ return in.getFileName().toString();
++ }
++
++ /**
++ * Returns the given path if the file was previously skipped for being remapped, otherwise the cached path or null.
++ *
++ * @param in input file
++ * @return {@code in} if already remapped, the cached path if present, otherwise null
++ */
++ @Nullable Path getIfPresent(final Path in) {
++ final String inHash = Hashing.sha256(in);
++ if (this.state.skippedHashes.contains(inHash)) {
++ return in;
++ }
++ return this.getIfPresent(inHash);
++ }
++
++ /**
++ * Returns the cached path if a remapped file is present for the given hash, otherwise null.
++ *
++ * @param inHash hash of the input file
++ * @return the cached path if present, otherwise null
++ * @see #getIfPresent(Path)
++ */
++ protected @Nullable Path getIfPresent(final String inHash) {
++ final @Nullable String fileName = this.state.hashes.get(inHash);
++ if (fileName == null) {
++ return null;
++ }
++
++ final Path path = this.dir.resolve(fileName);
++ if (Files.exists(path)) {
++ return path;
++ }
++ return null;
++ }
++
++ Path input(final Path in) {
++ return this.input(in, Hashing.sha256(in));
++ }
++
++ /**
++ * Marks the given file as skipped for remapping.
++ *
++ * @param in input file
++ */
++ void skip(final Path in) {
++ this.state.skippedHashes.add(Hashing.sha256(in));
++ }
++
++ protected Path input(final Path in, final String hashString) {
++ final String name = this.createCachedFileName(in);
++ this.state.hashes.put(hashString, name);
++ return this.dir.resolve(name);
++ }
++
++ void write() {
++ try (final BufferedWriter writer = AtomicFiles.atomicBufferedWriter(this.indexFile, StandardCharsets.UTF_8)) {
++ GSON.toJson(this.state, writer);
++ } catch (final IOException ex) {
++ LOGGER.warn("Failed to write index file '{}'", this.indexFile, ex);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java b/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aa5bf7ae042f3d43f7612d027ebef0e5c758ffc9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/ReobfServer.java
+@@ -0,0 +1,92 @@
++package io.papermc.paper.pluginremap;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.AtomicFiles;
++import io.papermc.paper.util.MappingEnvironment;
++import java.io.IOException;
++import java.net.URISyntaxException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.Executor;
++import net.neoforged.art.api.Renamer;
++import net.neoforged.art.api.Transformer;
++import net.neoforged.art.internal.RenamerImpl;
++import net.neoforged.srgutils.IMappingFile;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute;
++
++@DefaultQualifier(NonNull.class)
++final class ReobfServer {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final Path remapClasspathDir;
++ private final CompletableFuture<Void> load;
++
++ ReobfServer(final Path remapClasspathDir, final CompletableFuture<IMappingFile> mappings, final Executor executor) {
++ this.remapClasspathDir = remapClasspathDir;
++ if (this.mappingsChanged()) {
++ this.load = mappings.thenAcceptAsync(this::remap, executor);
++ } else {
++ if (PluginRemapper.DEBUG_LOGGING) {
++ LOGGER.info("Have cached reobf server for current mappings.");
++ }
++ this.load = CompletableFuture.completedFuture(null);
++ }
++ }
++
++ CompletableFuture<Path> remapped() {
++ return this.load.thenApply($ -> this.remappedPath());
++ }
++
++ private Path remappedPath() {
++ return this.remapClasspathDir.resolve(MappingEnvironment.mappingsHash() + ".jar");
++ }
++
++ private boolean mappingsChanged() {
++ return !Files.exists(this.remappedPath());
++ }
++
++ private void remap(final IMappingFile mappings) {
++ try {
++ if (!Files.exists(this.remapClasspathDir)) {
++ Files.createDirectories(this.remapClasspathDir);
++ }
++ for (final Path file : PluginRemapper.list(this.remapClasspathDir, Files::isRegularFile)) {
++ Files.delete(file);
++ }
++ } catch (final IOException ex) {
++ throw new RuntimeException(ex);
++ }
++
++ LOGGER.info("Remapping server...");
++ final long startRemap = System.currentTimeMillis();
++ try (final DebugLogger log = DebugLogger.forOutputFile(this.remappedPath())) {
++ AtomicFiles.atomicWrite(this.remappedPath(), writeTo -> {
++ try (final RenamerImpl renamer = (RenamerImpl) Renamer.builder()
++ .logger(log)
++ .debug(log.debug())
++ .threads(1)
++ .add(Transformer.renamerFactory(mappings, false))
++ .add(addNamespaceManifestAttribute(InsertManifestAttribute.SPIGOT_NAMESPACE))
++ .build()) {
++ renamer.run(serverJar().toFile(), writeTo.toFile(), true);
++ }
++ });
++ } catch (final Exception ex) {
++ throw new RuntimeException("Failed to remap server jar", ex);
++ }
++ LOGGER.info("Done remapping server in {}ms.", System.currentTimeMillis() - startRemap);
++ }
++
++ private static Path serverJar() {
++ try {
++ return Path.of(ReobfServer.class.getProtectionDomain().getCodeSource().getLocation().toURI());
++ } catch (final URISyntaxException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java b/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ad53aab4fee16b76f6e4fd44e4b28d06fef80de4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/UnknownOriginRemappedPluginIndex.java
+@@ -0,0 +1,72 @@
++package io.papermc.paper.pluginremap;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.Hashing;
++import java.io.IOException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.Map;
++import java.util.Set;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++@DefaultQualifier(NonNull.class)
++final class UnknownOriginRemappedPluginIndex extends RemappedPluginIndex {
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ private final Set<String> used = new HashSet<>();
++
++ UnknownOriginRemappedPluginIndex(final Path dir) {
++ super(dir, true);
++ }
++
++ @Override
++ @Nullable Path getIfPresent(final Path in) {
++ final String hash = Hashing.sha256(in);
++ if (this.state.skippedHashes.contains(hash)) {
++ return in;
++ }
++
++ final @Nullable Path path = super.getIfPresent(hash);
++ if (path != null) {
++ this.used.add(hash);
++ }
++ return path;
++ }
++
++ @Override
++ Path input(final Path in) {
++ final String hash = Hashing.sha256(in);
++ this.used.add(hash);
++ return super.input(in, hash);
++ }
++
++ void write(final boolean clean) {
++ if (!clean) {
++ super.write();
++ return;
++ }
++
++ final Iterator<Map.Entry<String, String>> it = this.state.hashes.entrySet().iterator();
++ while (it.hasNext()) {
++ final Map.Entry<String, String> next = it.next();
++ if (this.used.contains(next.getKey())) {
++ continue;
++ }
++
++ // Remove unused mapped file
++ it.remove();
++ final Path file = this.dir().resolve(next.getValue());
++ try {
++ Files.deleteIfExists(file);
++ } catch (final IOException ex) {
++ LOGGER.warn("Failed to delete no longer needed cached jar '{}'", file, ex);
++ }
++ }
++ super.write();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/AtomicFiles.java b/src/main/java/io/papermc/paper/util/AtomicFiles.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..944250d2b8e1969f221b2f24cce7b1019c55fe01
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/AtomicFiles.java
+@@ -0,0 +1,96 @@
++package io.papermc.paper.util;
++
++import java.io.IOException;
++import java.nio.file.AccessDeniedException;
++import java.nio.file.AtomicMoveNotSupportedException;
++import java.nio.file.CopyOption;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.StandardCopyOption;
++import java.util.concurrent.ThreadLocalRandom;
++import java.util.function.Consumer;
++import org.spongepowered.configurate.util.CheckedConsumer;
++
++// Stripped down version of https://github.com/jpenilla/squaremap/blob/7d7994b4096e5fc61364ea2d87e9aa4e14edf5c6/common/src/main/java/xyz/jpenilla/squaremap/common/util/FileUtil.java
++public final class AtomicFiles {
++
++ private AtomicFiles() {
++ }
++
++ public static void atomicWrite(final Path path, final CheckedConsumer<Path, IOException> op) throws IOException {
++ final Path tmp = tempFile(path);
++
++ try {
++ op.accept(tmp);
++ atomicMove(tmp, path, true);
++ } catch (final IOException ex) {
++ try {
++ Files.deleteIfExists(tmp);
++ } catch (final IOException ex1) {
++ ex.addSuppressed(ex1);
++ }
++ throw ex;
++ }
++ }
++
++ private static Path tempFile(final Path path) {
++ return path.resolveSibling("." + System.nanoTime() + "-" + ThreadLocalRandom.current().nextInt() + "-" + path.getFileName().toString() + ".tmp"); }
++
++ @SuppressWarnings("BusyWait") // not busy waiting
++ public static void atomicMove(final Path from, final Path to, final boolean replaceExisting) throws IOException {
++ final int maxRetries = 2;
++
++ try {
++ atomicMoveIfPossible(from, to, replaceExisting);
++ } catch (final AccessDeniedException ex) {
++ // Sometimes because of file locking this will fail... Let's just try again and hope for the best
++ // Thanks Windows!
++ int retries = 1;
++ while (true) {
++ try {
++ // Pause for a bit
++ Thread.sleep(10L * retries);
++ atomicMoveIfPossible(from, to, replaceExisting);
++ break; // success
++ } catch (final AccessDeniedException ex1) {
++ ex.addSuppressed(ex1);
++ if (retries == maxRetries) {
++ throw ex;
++ }
++ } catch (final InterruptedException interruptedException) {
++ ex.addSuppressed(interruptedException);
++ Thread.currentThread().interrupt();
++ throw ex;
++ }
++ ++retries;
++ }
++ }
++ }
++
++ private static void atomicMoveIfPossible(final Path from, final Path to, final boolean replaceExisting) throws IOException {
++ final CopyOption[] options = replaceExisting
++ ? new CopyOption[]{StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING}
++ : new CopyOption[]{StandardCopyOption.ATOMIC_MOVE};
++
++ try {
++ Files.move(from, to, options);
++ } catch (final AtomicMoveNotSupportedException ex) {
++ Files.move(from, to, replaceExisting ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{});
++ }
++ }
++
++ private static <T, X extends Throwable> Consumer<T> sneaky(final CheckedConsumer<T, X> consumer) {
++ return t -> {
++ try {
++ consumer.accept(t);
++ } catch (final Throwable thr) {
++ rethrow(thr);
++ }
++ };
++ }
++
++ @SuppressWarnings("unchecked")
++ private static <X extends Throwable> RuntimeException rethrow(final Throwable t) throws X {
++ throw (X) t;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/Hashing.java b/src/main/java/io/papermc/paper/util/Hashing.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..54c1324faa190a06bfa2b3b8f86928b4c51a57f8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/Hashing.java
+@@ -0,0 +1,50 @@
++package io.papermc.paper.util;
++
++import com.google.common.hash.HashCode;
++import java.io.IOException;
++import java.io.InputStream;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.Locale;
++import org.apache.commons.io.IOUtils;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class Hashing {
++ private Hashing() {
++ }
++
++ /**
++ * Hash the provided {@link InputStream} using SHA-256. Stream will be closed.
++ *
++ * @param stream input stream
++ * @return SHA-256 hash string
++ */
++ public static String sha256(final InputStream stream) {
++ try (stream) {
++ return com.google.common.hash.Hashing.sha256().hashBytes(IOUtils.toByteArray(stream)).toString().toUpperCase(Locale.ROOT);
++ } catch (final IOException ex) {
++ throw new RuntimeException("Failed to take hash of InputStream", ex);
++ }
++ }
++
++ /**
++ * Hash the provided file using SHA-256.
++ *
++ * @param file file
++ * @return SHA-256 hash string
++ */
++ public static String sha256(final Path file) {
++ if (!Files.isRegularFile(file)) {
++ throw new IllegalArgumentException("'" + file + "' is not a regular file!");
++ }
++ final HashCode hash;
++ try {
++ hash = com.google.common.io.Files.asByteSource(file.toFile()).hash(com.google.common.hash.Hashing.sha256());
++ } catch (final IOException ex) {
++ throw new RuntimeException("Failed to take hash of file '" + file + "'", ex);
++ }
++ return hash.toString().toUpperCase(Locale.ROOT);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/MappingEnvironment.java b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8e4229634d41a42b3d93948eebb77def7c0c72b1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
+@@ -0,0 +1,65 @@
++package io.papermc.paper.util;
++
++import java.io.InputStream;
++import java.util.Objects;
++import java.util.jar.Manifest;
++import net.minecraft.world.entity.MobCategory;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class MappingEnvironment {
++ private static final @Nullable String MAPPINGS_HASH = readMappingsHash();
++ private static final boolean REOBF = checkReobf();
++
++ private MappingEnvironment() {
++ }
++
++ public static boolean reobf() {
++ return REOBF;
++ }
++
++ public static boolean hasMappings() {
++ return MAPPINGS_HASH != null;
++ }
++
++ public static InputStream mappingsStream() {
++ return Objects.requireNonNull(mappingsStreamIfPresent(), "Missing mappings!");
++ }
++
++ public static @Nullable InputStream mappingsStreamIfPresent() {
++ return MappingEnvironment.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny");
++ }
++
++ public static String mappingsHash() {
++ return Objects.requireNonNull(MAPPINGS_HASH, "MAPPINGS_HASH");
++ }
++
++ private static @Nullable String readMappingsHash() {
++ final @Nullable Manifest manifest = JarManifests.manifest(MappingEnvironment.class);
++ if (manifest != null) {
++ final Object hash = manifest.getMainAttributes().getValue("Included-Mappings-Hash");
++ if (hash != null) {
++ return hash.toString();
++ }
++ }
++
++ final @Nullable InputStream stream = mappingsStreamIfPresent();
++ if (stream == null) {
++ return null;
++ }
++ return Hashing.sha256(stream);
++ }
++
++ @SuppressWarnings("ConstantConditions")
++ private static boolean checkReobf() {
++ final Class<?> clazz = MobCategory.class;
++ if (clazz.getSimpleName().equals("MobCategory")) {
++ return false;
++ } else if (clazz.getSimpleName().equals("EnumCreatureType")) {
++ return true;
++ }
++ throw new IllegalStateException();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/ObfHelper.java b/src/main/java/io/papermc/paper/util/ObfHelper.java
+index 9e6d48335b37fa5204bfebf396d748089884555b..6067be951c4c52c4b1da51efc01436b2c90ea3bf 100644
+--- a/src/main/java/io/papermc/paper/util/ObfHelper.java
++++ b/src/main/java/io/papermc/paper/util/ObfHelper.java
+@@ -80,10 +80,10 @@ public enum ObfHelper {
+ }
+
+ private static @Nullable Set<ClassMapping> loadMappingsIfPresent() {
+- try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
+- if (mappingsInputStream == null) {
+- return null;
+- }
++ if (!MappingEnvironment.hasMappings()) {
++ return null;
++ }
++ try (final InputStream mappingsInputStream = MappingEnvironment.mappingsStream()) {
+ final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot
+ final Set<ClassMapping> classes = new HashSet<>();
+
+diff --git a/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java b/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..badff5d6ae6dd8d209c82bc7e8afe370db6148f2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/concurrent/ScalingThreadPool.java
+@@ -0,0 +1,85 @@
++package io.papermc.paper.util.concurrent;
++
++import java.util.concurrent.BlockingQueue;
++import java.util.concurrent.LinkedBlockingQueue;
++import java.util.concurrent.RejectedExecutionHandler;
++import java.util.concurrent.ThreadPoolExecutor;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicInteger;
++
++/**
++ * Utilities for scaling thread pools.
++ *
++ * @see <a href="https://medium.com/@uditharosha/java-scale-first-executorservice-4245a63222df">Java Scale First ExecutorService — A myth or a reality</a>
++ */
++public final class ScalingThreadPool {
++ private ScalingThreadPool() {
++ }
++
++ public static RejectedExecutionHandler defaultReEnqueuePolicy() {
++ return reEnqueuePolicy(new ThreadPoolExecutor.AbortPolicy());
++ }
++
++ public static RejectedExecutionHandler reEnqueuePolicy(final RejectedExecutionHandler original) {
++ return new ReEnqueuePolicy(original);
++ }
++
++ public static <E> BlockingQueue<E> createUnboundedQueue() {
++ return new Queue<>();
++ }
++
++ public static <E> BlockingQueue<E> createQueue(final int capacity) {
++ return new Queue<>(capacity);
++ }
++
++ private static final class Queue<E> extends LinkedBlockingQueue<E> {
++ private final AtomicInteger idleThreads = new AtomicInteger(0);
++
++ private Queue() {
++ super();
++ }
++
++ private Queue(final int capacity) {
++ super(capacity);
++ }
++
++ @Override
++ public boolean offer(final E e) {
++ return this.idleThreads.get() > 0 && super.offer(e);
++ }
++
++ @Override
++ public E take() throws InterruptedException {
++ this.idleThreads.incrementAndGet();
++ try {
++ return super.take();
++ } finally {
++ this.idleThreads.decrementAndGet();
++ }
++ }
++
++ @Override
++ public E poll(final long timeout, final TimeUnit unit) throws InterruptedException {
++ this.idleThreads.incrementAndGet();
++ try {
++ return super.poll(timeout, unit);
++ } finally {
++ this.idleThreads.decrementAndGet();
++ }
++ }
++
++ @Override
++ public boolean add(final E e) {
++ return super.offer(e);
++ }
++ }
++
++ private record ReEnqueuePolicy(RejectedExecutionHandler originalHandler) implements RejectedExecutionHandler {
++ @Override
++ public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
++ if (!executor.getQueue().add(r)) {
++ this.originalHandler.rejectedExecution(r, executor);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index f43958a5253af0e753ba2b7d5ee9e715a3eaa424..8acd27005f2b7cf32dc4cf93cf1a6eea2ab594ca 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -657,6 +657,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+
+ this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
+ this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
+ this.connection.acceptConnections();
+ }
+@@ -930,6 +931,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.server.disablePlugins();
+ }
+ // CraftBukkit end
++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.shutdown(); // Paper - Plugin remapping
+ this.getConnection().stop();
+ this.isSaving = true;
+ if (this.playerList != null) {
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index 3b403e9edf4e860160dd230977870f21a0e32a7a..9d6be455c3bbcdbcb9d3d24b0bad79f46ba6a8cb 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -318,6 +318,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ }
+ }
+
++ // Paper start
++ public java.io.File getPluginsFolder() {
++ return (java.io.File) this.options.valueOf("plugins");
++ }
++ // Paper end
++
+ @Override
+ public boolean isSpawningMonsters() {
+ return this.settings.getProperties().spawnMonsters && super.isSpawningMonsters();
+diff --git a/src/main/java/net/neoforged/art/internal/RenamerImpl.java b/src/main/java/net/neoforged/art/internal/RenamerImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..73b20a92f330311e3fef8f03b51a098513afafc1
+--- /dev/null
++++ b/src/main/java/net/neoforged/art/internal/RenamerImpl.java
+@@ -0,0 +1,308 @@
++/*
++ * Forge Auto Renaming Tool
++ * Copyright (c) 2021
++ *
++ * This library is free software; you can redistribute it and/or
++ * modify it under the terms of the GNU Lesser General Public
++ * License as published by the Free Software Foundation version 2.1
++ * of the License.
++ *
++ * This library is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++ * Lesser General Public License for more details.
++ *
++ * You should have received a copy of the GNU Lesser General Public
++ * License along with this library; if not, write to the Free Software
++ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
++ */
++
++package net.neoforged.art.internal;
++
++import java.io.BufferedOutputStream;
++import java.io.ByteArrayOutputStream;
++import java.io.File;
++import java.io.IOException;
++import java.io.InputStream;
++import java.io.OutputStream;
++import java.nio.file.Files;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.Enumeration;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Objects;
++import java.util.Set;
++import java.util.function.Consumer;
++import java.util.stream.Collectors;
++import java.util.zip.ZipEntry;
++import java.util.zip.ZipFile;
++import java.util.zip.ZipOutputStream;
++
++import net.neoforged.cliutils.JarUtils;
++import net.neoforged.cliutils.progress.ProgressReporter;
++import org.objectweb.asm.Opcodes;
++
++import net.neoforged.art.api.ClassProvider;
++import net.neoforged.art.api.Renamer;
++import net.neoforged.art.api.Transformer;
++import net.neoforged.art.api.Transformer.ClassEntry;
++import net.neoforged.art.api.Transformer.Entry;
++import net.neoforged.art.api.Transformer.ManifestEntry;
++import net.neoforged.art.api.Transformer.ResourceEntry;
++
++public class RenamerImpl implements Renamer { // Paper - public
++ private static final ProgressReporter PROGRESS = ProgressReporter.getDefault();
++ static final int MAX_ASM_VERSION = Opcodes.ASM9;
++ private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
++ private final List<File> libraries;
++ private final List<Transformer> transformers;
++ private final SortedClassProvider sortedClassProvider;
++ private final List<ClassProvider> classProviders;
++ private final int threads;
++ private final Consumer<String> logger;
++ private final Consumer<String> debug;
++ private boolean setup = false;
++ private ClassProvider libraryClasses;
++
++ RenamerImpl(List<File> libraries, List<Transformer> transformers, SortedClassProvider sortedClassProvider, List<ClassProvider> classProviders,
++ int threads, Consumer<String> logger, Consumer<String> debug) {
++ this.libraries = libraries;
++ this.transformers = transformers;
++ this.sortedClassProvider = sortedClassProvider;
++ this.classProviders = Collections.unmodifiableList(classProviders);
++ this.threads = threads;
++ this.logger = logger;
++ this.debug = debug;
++ }
++
++ private void setup() {
++ if (this.setup)
++ return;
++
++ this.setup = true;
++
++ ClassProvider.Builder libraryClassesBuilder = ClassProvider.builder().shouldCacheAll(true);
++ this.logger.accept("Adding Libraries to Inheritance");
++ this.libraries.forEach(f -> libraryClassesBuilder.addLibrary(f.toPath()));
++
++ this.libraryClasses = libraryClassesBuilder.build();
++ }
++
++ @Override
++ public void run(File input, File output) {
++ // Paper start - Add remappingSelf
++ this.run(input, output, false);
++ }
++ public void run(File input, File output, boolean remappingSelf) {
++ // Paper end
++ if (!this.setup)
++ this.setup();
++
++ if (Boolean.getBoolean(ProgressReporter.ENABLED_PROPERTY)) {
++ try {
++ PROGRESS.setMaxProgress(JarUtils.getFileCountInZip(input));
++ } catch (IOException e) {
++ logger.accept("Failed to read zip file count: " + e);
++ }
++ }
++
++ input = Objects.requireNonNull(input).getAbsoluteFile();
++ output = Objects.requireNonNull(output).getAbsoluteFile();
++
++ if (!input.exists())
++ throw new IllegalArgumentException("Input file not found: " + input.getAbsolutePath());
++
++ logger.accept("Reading Input: " + input.getAbsolutePath());
++ PROGRESS.setStep("Reading input jar");
++ // Read everything from the input jar!
++ List<Entry> oldEntries = new ArrayList<>();
++ try (ZipFile in = new ZipFile(input)) {
++ int amount = 0;
++ for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements();) {
++ final ZipEntry e = entries.nextElement();
++ if (e.isDirectory())
++ continue;
++ String name = e.getName();
++ byte[] data;
++ try (InputStream entryInput = in.getInputStream(e)) {
++ data = entryInput.readAllBytes(); // Paper - Use readAllBytes
++ }
++
++ if (name.endsWith(".class") && !name.contains("META-INF/")) // Paper - Skip META-INF entries
++ oldEntries.add(ClassEntry.create(name, e.getTime(), data));
++ else if (name.equals(MANIFEST_NAME))
++ oldEntries.add(ManifestEntry.create(e.getTime(), data));
++ else if (name.equals("javadoctor.json"))
++ oldEntries.add(Transformer.JavadoctorEntry.create(e.getTime(), data));
++ else
++ oldEntries.add(ResourceEntry.create(name, e.getTime(), data));
++
++ if ((++amount) % 10 == 0) {
++ PROGRESS.setProgress(amount);
++ }
++ }
++ } catch (IOException e) {
++ throw new RuntimeException("Could not parse input: " + input.getAbsolutePath(), e);
++ }
++
++ this.sortedClassProvider.clearCache();
++ ArrayList<ClassProvider> classProviders = new ArrayList<>(this.classProviders);
++ classProviders.add(0, this.libraryClasses);
++ this.sortedClassProvider.classProviders = classProviders;
++
++ AsyncHelper async = new AsyncHelper(threads);
++ try {
++
++ /* Disabled until we do something with it
++ // Gather original file Hashes, so that we can detect changes and update the manifest if necessary
++ log("Gathering original hashes");
++ Map<String, String> oldHashes = async.invokeAll(oldEntries,
++ e -> new Pair<>(e.getName(), HashFunction.SHA256.hash(e.getData()))
++ ).stream().collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
++ */
++
++ PROGRESS.setProgress(0);
++ PROGRESS.setIndeterminate(true);
++ PROGRESS.setStep("Processing entries");
++
++ List<ClassEntry> ourClasses = oldEntries.stream()
++ .filter(e -> e instanceof ClassEntry && !e.getName().startsWith("META-INF/"))
++ .map(ClassEntry.class::cast)
++ .collect(Collectors.toList());
++
++ // Add the original classes to the inheritance map, TODO: Multi-Release somehow?
++ logger.accept("Adding input to inheritance map");
++ ClassProvider.Builder inputClassesBuilder = ClassProvider.builder();
++ async.consumeAll(ourClasses, ClassEntry::getClassName, c ->
++ inputClassesBuilder.addClass(c.getName().substring(0, c.getName().length() - 6), c.getData())
++ );
++ classProviders.add(0, inputClassesBuilder.build());
++
++ // Process everything
++ logger.accept("Processing entries");
++ List<Entry> newEntries = async.invokeAll(oldEntries, Entry::getName, this::processEntry);
++
++ logger.accept("Adding extras");
++ // Paper start - I'm pretty sure the duplicates are because the input is already on the classpath
++ List<Entry> finalNewEntries = newEntries;
++ transformers.forEach(t -> finalNewEntries.addAll(t.getExtras()));
++
++ Set<String> seen = new HashSet<>();
++ if (remappingSelf) {
++ // deduplicate
++ List<Entry> n = new ArrayList<>();
++ for (final Entry e : newEntries) {
++ if (seen.add(e.getName())) {
++ n.add(e);
++ }
++ }
++ newEntries = n;
++ } else {
++ String dupes = newEntries.stream().map(Entry::getName)
++ .filter(n -> !seen.add(n))
++ .sorted()
++ .collect(Collectors.joining(", "));
++ if (!dupes.isEmpty())
++ throw new IllegalStateException("Duplicate entries detected: " + dupes);
++ }
++ // Paper end
++
++ // We care about stable output, so sort, and single thread write.
++ logger.accept("Sorting");
++ Collections.sort(newEntries, this::compare);
++
++ if (!output.getParentFile().exists())
++ output.getParentFile().mkdirs();
++
++ seen.clear();
++
++ PROGRESS.setMaxProgress(newEntries.size());
++ PROGRESS.setStep("Writing output");
++
++ logger.accept("Writing Output: " + output.getAbsolutePath());
++ try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(output.toPath()));
++ ZipOutputStream zos = new ZipOutputStream(fos)) {
++
++ int amount = 0;
++ for (Entry e : newEntries) {
++ String name = e.getName();
++ int idx = name.lastIndexOf('/');
++ if (idx != -1)
++ addDirectory(zos, seen, name.substring(0, idx));
++
++ logger.accept(" " + name);
++ ZipEntry entry = new ZipEntry(name);
++ entry.setTime(e.getTime());
++ zos.putNextEntry(entry);
++ zos.write(e.getData());
++ zos.closeEntry();
++
++ if ((++amount) % 10 == 0) {
++ PROGRESS.setProgress(amount);
++ }
++ }
++
++ PROGRESS.setProgress(amount);
++ }
++ } catch (final IOException e) {
++ throw new RuntimeException("Could not write to file " + output.getAbsolutePath(), e);
++ } finally {
++ async.shutdown();
++ }
++ }
++
++ private byte[] readAllBytes(InputStream in, long size) throws IOException {
++ // This program will crash if size exceeds MAX_INT anyway since arrays are limited to 32-bit indices
++ ByteArrayOutputStream tmp = new ByteArrayOutputStream(size >= 0 ? (int) size : 0);
++
++ byte[] buffer = new byte[8192];
++ int read;
++ while ((read = in.read(buffer)) != -1) {
++ tmp.write(buffer, 0, read);
++ }
++
++ return tmp.toByteArray();
++ }
++
++ // Tho Directory entries are not strictly necessary, we add them because some bad implementations of Zip extractors
++ // attempt to extract files without making sure the parents exist.
++ private void addDirectory(ZipOutputStream zos, Set<String> seen, String path) throws IOException {
++ if (!seen.add(path))
++ return;
++
++ int idx = path.lastIndexOf('/');
++ if (idx != -1)
++ addDirectory(zos, seen, path.substring(0, idx));
++
++ logger.accept(" " + path + '/');
++ ZipEntry dir = new ZipEntry(path + '/');
++ dir.setTime(Entry.STABLE_TIMESTAMP);
++ zos.putNextEntry(dir);
++ zos.closeEntry();
++ }
++
++ private Entry processEntry(final Entry start) {
++ Entry entry = start;
++ for (Transformer transformer : RenamerImpl.this.transformers) {
++ entry = entry.process(transformer);
++ if (entry == null)
++ return null;
++ }
++ return entry;
++ }
++
++ private int compare(Entry o1, Entry o2) {
++ // In order for JarInputStream to work, MANIFEST has to be the first entry, so make it first!
++ if (MANIFEST_NAME.equals(o1.getName()))
++ return MANIFEST_NAME.equals(o2.getName()) ? 0 : -1;
++ if (MANIFEST_NAME.equals(o2.getName()))
++ return MANIFEST_NAME.equals(o1.getName()) ? 0 : 1;
++ return o1.getName().compareTo(o2.getName());
++ }
++
++ @Override
++ public void close() throws IOException {
++ this.sortedClassProvider.close();
++ }
++}
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 542ff64ce0cb93a9f996fa0a65e8dde7ed39c3a9..5c54c5c525c86bb8037982435b8769ec2ca2c6cb 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -1005,6 +1005,7 @@ public final class CraftServer implements Server {
+ this.loadPlugins();
+ this.enablePlugins(PluginLoadOrder.STARTUP);
+ this.enablePlugins(PluginLoadOrder.POSTWORLD);
++ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
+ this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD));
+ }
+
diff --git a/patches/server/0021-Hook-into-CB-plugin-rewrites.patch b/patches/server/0021-Hook-into-CB-plugin-rewrites.patch
new file mode 100644
index 0000000000..213edbe281
--- /dev/null
+++ b/patches/server/0021-Hook-into-CB-plugin-rewrites.patch
@@ -0,0 +1,185 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Wed, 3 Oct 2018 20:09:18 -0400
+Subject: [PATCH] Hook into CB plugin rewrites
+
+Allows us to do fun stuff like rewrite the OBC util fastutil location to
+our own relocation. Also lets us rewrite NMS calls for when we're
+debugging in an IDE pre-relocate.
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+index 40fbbff1df7e65ca01bb82e213eeefbb38c85a7a..7d2d5c4ee244e7118a4c38628e2e69c79b11b98d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+@@ -12,6 +12,7 @@ import java.util.Arrays;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.Enumeration;
++import java.util.HashMap;
+ import java.util.HashSet;
+ import java.util.List;
+ import java.util.Map;
+@@ -22,6 +23,7 @@ import java.util.jar.JarEntry;
+ import java.util.jar.JarFile;
+ import java.util.jar.JarOutputStream;
+ import java.util.zip.ZipEntry;
++import javax.annotation.Nonnull;
+ import joptsimple.OptionParser;
+ import joptsimple.OptionSet;
+ import joptsimple.OptionSpec;
+@@ -128,6 +130,40 @@ public class Commodore {
+ return this.reroutes;
+ }
+
++ // Paper start - Plugin rewrites
++ private static final Map<String, String> SEARCH_AND_REMOVE = initReplacementsMap();
++ private static Map<String, String> initReplacementsMap() {
++ Map<String, String> getAndRemove = new HashMap<>();
++ // Be wary of maven shade's relocations
++
++ final java.util.jar.Manifest manifest = io.papermc.paper.util.JarManifests.manifest(Commodore.class);
++ if (Boolean.getBoolean( "debug.rewriteForIde") && manifest != null)
++ {
++ // unversion incoming calls for pre-relocate debug work
++ final String NMS_REVISION_PACKAGE = "v" + manifest.getMainAttributes().getValue("CraftBukkit-Package-Version") + "/";
++
++ getAndRemove.put("org/bukkit/".concat("craftbukkit/" + NMS_REVISION_PACKAGE), NMS_REVISION_PACKAGE);
++ }
++
++ return getAndRemove;
++ }
++
++ @Nonnull
++ private static String getOriginalOrRewrite(@Nonnull String original)
++ {
++ String rewrite = null;
++ for ( Map.Entry<String, String> entry : SEARCH_AND_REMOVE.entrySet() )
++ {
++ if ( original.contains( entry.getKey() ) )
++ {
++ rewrite = original.replace( entry.getValue(), "" );
++ }
++ }
++
++ return rewrite != null ? rewrite : original;
++ }
++ // Paper end - Plugin rewrites
++
+ public static void main(String[] args) {
+ OptionParser parser = new OptionParser();
+ OptionSpec<File> inputFlag = parser.acceptsAll(Arrays.asList("i", "input")).withRequiredArg().ofType(File.class).required();
+@@ -283,9 +319,49 @@ public class Commodore {
+ }
+
+ return new MethodVisitor(this.api, super.visitMethod(access, name, desc, signature, exceptions)) {
++ // Paper start - Plugin rewrites
++ @Override
++ public void visitTypeInsn(int opcode, String type) {
++ type = getOriginalOrRewrite(type);
++
++ super.visitTypeInsn(opcode, type);
++ }
++
++ @Override
++ public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
++ for (int i = 0; i < local.length; i++)
++ {
++ if (!(local[i] instanceof String)) { continue; }
++
++ local[i] = getOriginalOrRewrite((String) local[i]);
++ }
++
++ for (int i = 0; i < stack.length; i++)
++ {
++ if (!(stack[i] instanceof String)) { continue; }
++
++ stack[i] = getOriginalOrRewrite((String) stack[i]);
++ }
++
++ super.visitFrame(type, nLocal, local, nStack, stack);
++ }
++
++ @Override
++ public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
++ descriptor = getOriginalOrRewrite(descriptor);
++
++ super.visitLocalVariable(name, descriptor, signature, start, end, index);
++ }
++ // Paper end - Plugin rewrites
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
++ // Paper start - Rewrite plugins
++ owner = getOriginalOrRewrite(owner);
++ if (desc != null) {
++ desc = getOriginalOrRewrite(desc);
++ }
++ // Paper end
+ name = FieldRename.rename(pluginVersion, owner, name);
+
+ if (modern) {
+@@ -398,6 +474,13 @@ public class Commodore {
+ return;
+ }
+
++ // Paper start - Rewrite plugins
++ owner = getOriginalOrRewrite(owner) ;
++ if (desc != null) {
++ desc = getOriginalOrRewrite(desc);
++ }
++ // Paper end - Rewrite plugins
++
+ if (modern) {
+ if (owner.equals("org/bukkit/Material") || (instantiatedMethodType != null && instantiatedMethodType.getDescriptor().startsWith("(Lorg/bukkit/Material;)"))) {
+ switch (name) {
+@@ -494,6 +577,13 @@ public class Commodore {
+
+ @Override
+ public void visitLdcInsn(Object value) {
++ // Paper start
++ if (value instanceof Type type) {
++ if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
++ value = Type.getType(getOriginalOrRewrite(type.getDescriptor()));
++ }
++ }
++ // Paper end
+ if (value instanceof String && ((String) value).equals("com.mysql.jdbc.Driver")) {
+ super.visitLdcInsn("com.mysql.cj.jdbc.Driver");
+ return;
+@@ -504,6 +594,14 @@ public class Commodore {
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
++ // Paper start - Rewrite plugins
++ name = getOriginalOrRewrite(name);
++ if (descriptor != null) {
++ descriptor = getOriginalOrRewrite(descriptor);
++ }
++ final String fName = name;
++ final String fDescriptor = descriptor;
++ // Paper end - Rewrite plugins
+ if (bootstrapMethodHandle.getOwner().equals("java/lang/invoke/LambdaMetafactory")
+ && bootstrapMethodHandle.getName().equals("metafactory") && bootstrapMethodArguments.length == 3) {
+ Type samMethodType = (Type) bootstrapMethodArguments[0];
+@@ -520,7 +618,7 @@ public class Commodore {
+ methodArgs.add(new Handle(newOpcode, newOwner, newName, newDescription, newItf));
+ methodArgs.add(newInstantiated);
+
+- super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, methodArgs.toArray(Object[]::new));
++ super.visitInvokeDynamicInsn(fName, fDescriptor, bootstrapMethodHandle, methodArgs.toArray(Object[]::new)); // Paper - use final local vars
+ }, implMethod.getTag(), implMethod.getOwner(), implMethod.getName(), implMethod.getDesc(), implMethod.isInterface(), samMethodType, instantiatedMethodType);
+ return;
+ }
+@@ -571,6 +669,12 @@ public class Commodore {
+
+ @Override
+ public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
++ // Paper start - Rewrite plugins
++ descriptor = getOriginalOrRewrite(descriptor);
++ if ( signature != null ) {
++ signature = getOriginalOrRewrite(signature);
++ }
++ // Paper end
+ return new FieldVisitor(this.api, super.visitField(access, name, descriptor, signature, value)) {
+ @Override
+ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
diff --git a/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch
new file mode 100644
index 0000000000..ebcc5735fb
--- /dev/null
+++ b/patches/server/0022-Remap-reflection-calls-in-plugins-using-internals.patch
@@ -0,0 +1,740 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Nassim Jahnke <[email protected]>
+Date: Sun, 30 Oct 2022 23:47:26 +0100
+Subject: [PATCH] Remap reflection calls in plugins using internals
+
+Co-authored-by: Jason Penilla <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 884ac16677ee3f52174c7bbf7b34896bcbf04bbc..49749e2bd8a4af96d2091fa1bccd876c2abb9e12 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -62,6 +62,12 @@ dependencies {
+ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
+ implementation("net.neoforged:srgutils:1.0.9") // Paper - mappings handling
+ implementation("net.neoforged:AutoRenamingTool:2.0.3") // Paper - remap plugins
++ // Paper start - Remap reflection
++ val reflectionRewriterVersion = "0.0.3"
++ implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion")
++ implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion")
++ implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion")
++ // Paper end - Remap reflection
+ }
+
+ paperweight {
+diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
+index b61935052154e76b1b8cb49868c96c52f34a41d1..a2fe12513b93ded71517955ef3e52c925f56f7d1 100644
+--- a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
++++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
+@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableBiMap;
+ import com.mojang.logging.LogUtils;
+ import io.leangen.geantyref.TypeToken;
+ import io.papermc.paper.configuration.serializer.collections.MapSerializer;
++import io.papermc.paper.util.MappingEnvironment;
+ import io.papermc.paper.util.ObfHelper;
+ import java.lang.reflect.Type;
+ import java.util.List;
+@@ -68,7 +69,7 @@ public final class PacketClassSerializer extends ScalarSerializer<Class<? extend
+ @Override
+ protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) {
+ final String name = packetClass.getName();
+- @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
++ @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null || !MappingEnvironment.reobf() ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
+ if (mojName == null && MOJANG_TO_OBF.containsKey(name)) {
+ mojName = name;
+ }
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1240c061c121e8d5eb9add4e5e21955ee6df9368
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
+@@ -0,0 +1,187 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.pluginremap.reflect.ReflectionRemapper;
++import java.io.IOException;
++import java.io.InputStream;
++import java.io.UncheckedIOException;
++import java.net.JarURLConnection;
++import java.net.URI;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.security.CodeSigner;
++import java.security.CodeSource;
++import java.util.Map;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.function.Function;
++import java.util.jar.Attributes;
++import java.util.jar.Manifest;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.objectweb.asm.ClassReader;
++import org.objectweb.asm.ClassVisitor;
++import org.objectweb.asm.ClassWriter;
++
++import static java.util.Objects.requireNonNullElse;
++
++public final class BytecodeModifyingURLClassLoader extends URLClassLoader {
++ static {
++ ClassLoader.registerAsParallelCapable();
++ }
++
++ private static final Object MISSING_MANIFEST = new Object();
++
++ private final Function<byte[], byte[]> modifier;
++ private final Map<String, Object> manifests = new ConcurrentHashMap<>();
++
++ public BytecodeModifyingURLClassLoader(
++ final URL[] urls,
++ final ClassLoader parent,
++ final Function<byte[], byte[]> modifier
++ ) {
++ super(urls, parent);
++ this.modifier = modifier;
++ }
++
++ public BytecodeModifyingURLClassLoader(
++ final URL[] urls,
++ final ClassLoader parent
++ ) {
++ this(urls, parent, bytes -> {
++ final ClassReader classReader = new ClassReader(bytes);
++ final ClassWriter classWriter = new ClassWriter(classReader, 0);
++ final ClassVisitor visitor = ReflectionRemapper.visitor(classWriter);
++ if (visitor == classWriter) {
++ return bytes;
++ }
++ classReader.accept(visitor, 0);
++ return classWriter.toByteArray();
++ });
++ }
++
++ @Override
++ protected Class<?> findClass(final String name) throws ClassNotFoundException {
++ final Class<?> result;
++ final String path = name.replace('.', '/').concat(".class");
++ final URL url = this.findResource(path);
++ if (url != null) {
++ try {
++ result = this.defineClass(name, url);
++ } catch (final IOException e) {
++ throw new ClassNotFoundException(name, e);
++ }
++ } else {
++ result = null;
++ }
++ if (result == null) {
++ throw new ClassNotFoundException(name);
++ }
++ return result;
++ }
++
++ private Class<?> defineClass(String name, URL url) throws IOException {
++ int i = name.lastIndexOf('.');
++ if (i != -1) {
++ String pkgname = name.substring(0, i);
++ // Check if package already loaded.
++ final @Nullable Manifest man = this.manifestFor(url);
++ final URL jarUrl = URI.create(jarName(url)).toURL();
++ if (this.getAndVerifyPackage(pkgname, man, jarUrl) == null) {
++ try {
++ if (man != null) {
++ this.definePackage(pkgname, man, jarUrl);
++ } else {
++ this.definePackage(pkgname, null, null, null, null, null, null, null);
++ }
++ } catch (IllegalArgumentException iae) {
++ // parallel-capable class loaders: re-verify in case of a
++ // race condition
++ if (this.getAndVerifyPackage(pkgname, man, jarUrl) == null) {
++ // Should never happen
++ throw new AssertionError("Cannot find package " +
++ pkgname);
++ }
++ }
++ }
++ }
++ final byte[] bytes;
++ try (final InputStream is = url.openStream()) {
++ bytes = is.readAllBytes();
++ }
++
++ final byte[] modified = this.modifier.apply(bytes);
++
++ final CodeSource cs = new CodeSource(url, (CodeSigner[]) null);
++ return this.defineClass(name, modified, 0, modified.length, cs);
++ }
++
++ private Package getAndVerifyPackage(
++ String pkgname,
++ Manifest man, URL url
++ ) {
++ Package pkg = getDefinedPackage(pkgname);
++ if (pkg != null) {
++ // Package found, so check package sealing.
++ if (pkg.isSealed()) {
++ // Verify that code source URL is the same.
++ if (!pkg.isSealed(url)) {
++ throw new SecurityException(
++ "sealing violation: package " + pkgname + " is sealed");
++ }
++ } else {
++ // Make sure we are not attempting to seal the package
++ // at this code source URL.
++ if ((man != null) && this.isSealed(pkgname, man)) {
++ throw new SecurityException(
++ "sealing violation: can't seal package " + pkgname +
++ ": already loaded");
++ }
++ }
++ }
++ return pkg;
++ }
++
++ private boolean isSealed(String name, Manifest man) {
++ Attributes attr = man.getAttributes(name.replace('.', '/').concat("/"));
++ String sealed = null;
++ if (attr != null) {
++ sealed = attr.getValue(Attributes.Name.SEALED);
++ }
++ if (sealed == null) {
++ if ((attr = man.getMainAttributes()) != null) {
++ sealed = attr.getValue(Attributes.Name.SEALED);
++ }
++ }
++ return "true".equalsIgnoreCase(sealed);
++ }
++
++ private @Nullable Manifest manifestFor(final URL url) throws IOException {
++ Manifest man = null;
++ if (url.getProtocol().equals("jar")) {
++ try {
++ final Object computedManifest = this.manifests.computeIfAbsent(jarName(url), $ -> {
++ try {
++ final Manifest m = ((JarURLConnection) url.openConnection()).getManifest();
++ return requireNonNullElse(m, MISSING_MANIFEST);
++ } catch (final IOException e) {
++ throw new UncheckedIOException(e);
++ }
++ });
++ if (computedManifest instanceof Manifest found) {
++ man = found;
++ }
++ } catch (final UncheckedIOException e) {
++ throw e.getCause();
++ } catch (final IllegalArgumentException e) {
++ throw new IOException(e);
++ }
++ }
++ return man;
++ }
++
++ private static String jarName(final URL sourceUrl) {
++ final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!');
++ if (exclamationIdx != -1) {
++ return sourceUrl.getPath().substring(0, exclamationIdx);
++ }
++ throw new IllegalArgumentException("Could not find jar for URL " + sourceUrl);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+index f9a2c55a354c877749db3f92956de802ae575788..0e734c07dbe82ba4c319a237f9e79b08b57b997f 100644
+--- a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+@@ -7,6 +7,6 @@ public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModi
+
+ @Override
+ public byte[] modify(PluginMeta configuration, byte[] bytecode) {
+- return bytecode;
++ return io.papermc.paper.pluginremap.reflect.ReflectionRemapper.processClass(bytecode);
+ }
+ }
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+index f576060c8fe872772bbafe2016fc9b83a3c095f1..f871a329eb52da077f58d0ceaaabd3349f84cad0 100644
+--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -2,12 +2,12 @@ package io.papermc.paper.plugin.loader;
+
+ import io.papermc.paper.plugin.PluginInitializerManager;
+ import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+ import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
+ import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
+-import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+ import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+-import org.jetbrains.annotations.NotNull;
+-
++import io.papermc.paper.util.MappingEnvironment;
+ import java.io.IOException;
+ import java.net.MalformedURLException;
+ import java.net.URL;
+@@ -17,6 +17,7 @@ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.jar.JarFile;
+ import java.util.logging.Logger;
++import org.jetbrains.annotations.NotNull;
+
+ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+
+@@ -60,7 +61,10 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+ }
+
+ try {
+- return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
++ final URLClassLoader libraryLoader = MappingEnvironment.DISABLE_PLUGIN_REMAPPING
++ ? new URLClassLoader(urls, this.getClass().getClassLoader())
++ : new BytecodeModifyingURLClassLoader(urls, this.getClass().getClassLoader());
++ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), libraryLoader);
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+index bdd9bc8a414719b9f1d6f01f90539ddb8603a878..1bf0fa1530b8e5f94d726d0313b7a00f675b500c 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+@@ -1,9 +1,12 @@
+ package io.papermc.paper.plugin.provider.type.spigot;
+
++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
+ import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
+ import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import io.papermc.paper.util.MappingEnvironment;
+ import org.bukkit.plugin.InvalidDescriptionException;
+ import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.LibraryLoader;
+ import org.yaml.snakeyaml.error.YAMLException;
+
+ import java.io.IOException;
+@@ -15,6 +18,12 @@ import java.util.jar.JarFile;
+
+ class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
+
++ static {
++ if (!MappingEnvironment.DISABLE_PLUGIN_REMAPPING) {
++ LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new;
++ }
++ }
++
+ @Override
+ public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
+ // Copied from SimplePluginManager#loadPlugins
+diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..92bc8e4933ff13764fa2ac7f3729216332e202c9
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
+@@ -0,0 +1,211 @@
++package io.papermc.paper.pluginremap.reflect;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.MappingEnvironment;
++import io.papermc.paper.util.ObfHelper;
++import io.papermc.reflectionrewriter.runtime.AbstractDefaultRulesReflectionProxy;
++import io.papermc.reflectionrewriter.runtime.DefineClassReflectionProxy;
++import java.lang.invoke.MethodHandles;
++import java.nio.ByteBuffer;
++import java.security.CodeSource;
++import java.security.ProtectionDomain;
++import java.util.Map;
++import java.util.Objects;
++import java.util.stream.Collectors;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++// todo proper inheritance handling
++@SuppressWarnings("unused")
++@DefaultQualifier(NonNull.class)
++public final class PaperReflection extends AbstractDefaultRulesReflectionProxy implements DefineClassReflectionProxy {
++ // concat to avoid being rewritten by shadow
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final String CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit.");
++ private static final String LEGACY_CB_PACKAGE_PREFIX = "org.bukkit.".concat("craftbukkit.") + MappingEnvironment.LEGACY_CB_VERSION + ".";
++
++ private final DefineClassReflectionProxy defineClassProxy;
++ private final Map<String, ObfHelper.ClassMapping> mappingsByMojangName;
++ private final Map<String, ObfHelper.ClassMapping> mappingsByObfName;
++ // Reflection does not care about method return values, so this map removes the return value descriptor from the key
++ private final Map<String, Map<String, String>> strippedMethodMappings;
++
++ PaperReflection() {
++ this.defineClassProxy = DefineClassReflectionProxy.create(PaperReflection::processClass);
++ if (!MappingEnvironment.hasMappings()) {
++ this.mappingsByMojangName = Map.of();
++ this.mappingsByObfName = Map.of();
++ this.strippedMethodMappings = Map.of();
++ return;
++ }
++ final ObfHelper obfHelper = ObfHelper.INSTANCE;
++ this.mappingsByMojangName = Objects.requireNonNull(obfHelper.mappingsByMojangName(), "mappingsByMojangName");
++ this.mappingsByObfName = Objects.requireNonNull(obfHelper.mappingsByObfName(), "mappingsByObfName");
++ this.strippedMethodMappings = this.mappingsByMojangName.entrySet().stream().collect(Collectors.toUnmodifiableMap(
++ Map.Entry::getKey,
++ entry -> entry.getValue().strippedMethods()
++ ));
++ }
++
++ @Override
++ protected String mapClassName(final String name) {
++ final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByObfName.get(name);
++ return mapping != null ? mapping.mojangName() : removeCraftBukkitRelocation(name);
++ }
++
++ @Override
++ protected String mapDeclaredMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
++ final @Nullable Map<String, String> mapping = this.strippedMethodMappings.get(clazz.getName());
++ if (mapping == null) {
++ return name;
++ }
++ return mapping.getOrDefault(strippedMethodKey(name, parameterTypes), name);
++ }
++
++ @Override
++ protected String mapMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
++ final @Nullable String mapped = this.findMappedMethodName(clazz, name, parameterTypes);
++ return mapped != null ? mapped : name;
++ }
++
++ @Override
++ protected String mapDeclaredFieldName(final Class<?> clazz, final String name) {
++ final ObfHelper.@Nullable ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName());
++ if (mapping == null) {
++ return name;
++ }
++ return mapping.fieldsByObf().getOrDefault(name, name);
++ }
++
++ @Override
++ protected String mapFieldName(final Class<?> clazz, final String name) {
++ final @Nullable String mapped = this.findMappedFieldName(clazz, name);
++ return mapped != null ? mapped : name;
++ }
++
++ private @Nullable String findMappedMethodName(final Class<?> clazz, final String name, final Class<?> @Nullable ... parameterTypes) {
++ final Map<String, String> map = this.strippedMethodMappings.get(clazz.getName());
++ @Nullable String mapped = null;
++ if (map != null) {
++ mapped = map.get(strippedMethodKey(name, parameterTypes));
++ if (mapped != null) {
++ return mapped;
++ }
++ }
++ // JVM checks super before interfaces
++ final Class<?> superClass = clazz.getSuperclass();
++ if (superClass != null) {
++ mapped = this.findMappedMethodName(superClass, name, parameterTypes);
++ }
++ if (mapped == null) {
++ for (final Class<?> i : clazz.getInterfaces()) {
++ mapped = this.findMappedMethodName(i, name, parameterTypes);
++ if (mapped != null) {
++ break;
++ }
++ }
++ }
++ return mapped;
++ }
++
++ private @Nullable String findMappedFieldName(final Class<?> clazz, final String name) {
++ final ObfHelper.ClassMapping mapping = this.mappingsByMojangName.get(clazz.getName());
++ @Nullable String mapped = null;
++ if (mapping != null) {
++ mapped = mapping.fieldsByObf().get(name);
++ if (mapped != null) {
++ return mapped;
++ }
++ }
++ // The JVM checks super before interfaces
++ final Class<?> superClass = clazz.getSuperclass();
++ if (superClass != null) {
++ mapped = this.findMappedFieldName(superClass, name);
++ }
++ if (mapped == null) {
++ for (final Class<?> i : clazz.getInterfaces()) {
++ mapped = this.findMappedFieldName(i, name);
++ if (mapped != null) {
++ break;
++ }
++ }
++ }
++ return mapped;
++ }
++
++ private static String strippedMethodKey(final String methodName, final Class<?> @Nullable ... parameterTypes) {
++ return methodName + parameterDescriptor(parameterTypes);
++ }
++
++ private static String parameterDescriptor(final Class<?> @Nullable ... parameterTypes) {
++ if (parameterTypes == null) {
++ // Null parameterTypes is treated as an empty array
++ return "()";
++ }
++ final StringBuilder builder = new StringBuilder();
++ builder.append('(');
++ for (final Class<?> parameterType : parameterTypes) {
++ builder.append(parameterType.descriptorString());
++ }
++ builder.append(')');
++ return builder.toString();
++ }
++
++ private static String removeCraftBukkitRelocation(final String name) {
++ if (MappingEnvironment.hasMappings()) {
++ // Relocation is applied in reobf, and when mappings are present they handle the relocation
++ return name;
++ }
++ if (name.startsWith(LEGACY_CB_PACKAGE_PREFIX)) {
++ return CB_PACKAGE_PREFIX + name.substring(LEGACY_CB_PACKAGE_PREFIX.length());
++ }
++ return name;
++ }
++
++ @Override
++ public Class<?> defineClass(final Object loader, final byte[] b, final int off, final int len) throws ClassFormatError {
++ return this.defineClassProxy.defineClass(loader, b, off, len);
++ }
++
++ @Override
++ public Class<?> defineClass(final Object loader, final String name, final byte[] b, final int off, final int len) throws ClassFormatError {
++ return this.defineClassProxy.defineClass(loader, name, b, off, len);
++ }
++
++ @Override
++ public Class<?> defineClass(final Object loader, final @Nullable String name, final byte[] b, final int off, final int len, final @Nullable ProtectionDomain protectionDomain) throws ClassFormatError {
++ return this.defineClassProxy.defineClass(loader, name, b, off, len, protectionDomain);
++ }
++
++ @Override
++ public Class<?> defineClass(final Object loader, final String name, final ByteBuffer b, final ProtectionDomain protectionDomain) throws ClassFormatError {
++ return this.defineClassProxy.defineClass(loader, name, b, protectionDomain);
++ }
++
++ @Override
++ public Class<?> defineClass(final Object secureLoader, final String name, final byte[] b, final int off, final int len, final CodeSource cs) {
++ return this.defineClassProxy.defineClass(secureLoader, name, b, off, len, cs);
++ }
++
++ @Override
++ public Class<?> defineClass(final Object secureLoader, final String name, final ByteBuffer b, final CodeSource cs) {
++ return this.defineClassProxy.defineClass(secureLoader, name, b, cs);
++ }
++
++ @Override
++ public Class<?> defineClass(final MethodHandles.Lookup lookup, final byte[] bytes) throws IllegalAccessException {
++ return this.defineClassProxy.defineClass(lookup, bytes);
++ }
++
++ // todo apply bytecode remap here as well
++ private static byte[] processClass(final byte[] bytes) {
++ try {
++ return ReflectionRemapper.processClass(bytes);
++ } catch (final Exception ex) {
++ LOGGER.warn("Failed to process class bytes", ex);
++ return bytes;
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a3045afbc0cc057e99189b909367b21cf6a9e03f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/pluginremap/reflect/ReflectionRemapper.java
+@@ -0,0 +1,66 @@
++package io.papermc.paper.pluginremap.reflect;
++
++import io.papermc.asm.ClassInfoProvider;
++import io.papermc.asm.RewriteRuleVisitorFactory;
++import io.papermc.paper.util.MappingEnvironment;
++import io.papermc.reflectionrewriter.BaseReflectionRules;
++import io.papermc.reflectionrewriter.DefineClassRule;
++import io.papermc.reflectionrewriter.proxygenerator.ProxyGenerator;
++import java.lang.invoke.MethodHandles;
++import java.lang.reflect.Method;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.objectweb.asm.ClassReader;
++import org.objectweb.asm.ClassVisitor;
++import org.objectweb.asm.ClassWriter;
++import org.objectweb.asm.Opcodes;
++
++@DefaultQualifier(NonNull.class)
++public final class ReflectionRemapper {
++ private static final String PAPER_REFLECTION_HOLDER = "io.papermc.paper.pluginremap.reflect.PaperReflectionHolder";
++ private static final String PAPER_REFLECTION_HOLDER_DESC = PAPER_REFLECTION_HOLDER.replace('.', '/');
++ private static final RewriteRuleVisitorFactory VISITOR_FACTORY = RewriteRuleVisitorFactory.create(
++ Opcodes.ASM9,
++ chain -> chain.then(new BaseReflectionRules(PAPER_REFLECTION_HOLDER).rules())
++ .then(DefineClassRule.create(PAPER_REFLECTION_HOLDER_DESC, true)),
++ ClassInfoProvider.basic()
++ );
++
++ static {
++ if (!MappingEnvironment.reobf()) {
++ setupProxy();
++ }
++ }
++
++ private ReflectionRemapper() {
++ }
++
++ public static ClassVisitor visitor(final ClassVisitor parent) {
++ if (MappingEnvironment.reobf() || MappingEnvironment.DISABLE_PLUGIN_REMAPPING) {
++ return parent;
++ }
++ return VISITOR_FACTORY.createVisitor(parent);
++ }
++
++ public static byte[] processClass(final byte[] bytes) {
++ if (MappingEnvironment.DISABLE_PLUGIN_REMAPPING) {
++ return bytes;
++ }
++ final ClassReader classReader = new ClassReader(bytes);
++ final ClassWriter classWriter = new ClassWriter(classReader, 0);
++ classReader.accept(ReflectionRemapper.visitor(classWriter), 0);
++ return classWriter.toByteArray();
++ }
++
++ private static void setupProxy() {
++ try {
++ final byte[] bytes = ProxyGenerator.generateProxy(PaperReflection.class, PAPER_REFLECTION_HOLDER_DESC);
++ final MethodHandles.Lookup lookup = MethodHandles.lookup();
++ final Class<?> generated = lookup.defineClass(bytes);
++ final Method init = generated.getDeclaredMethod("init", PaperReflection.class);
++ init.invoke(null, new PaperReflection());
++ } catch (final ReflectiveOperationException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/util/MappingEnvironment.java b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
+index 8e4229634d41a42b3d93948eebb77def7c0c72b1..4477944f632a6b3936960ee80f9d898d3b7eed19 100644
+--- a/src/main/java/io/papermc/paper/util/MappingEnvironment.java
++++ b/src/main/java/io/papermc/paper/util/MappingEnvironment.java
+@@ -10,6 +10,8 @@ import org.checkerframework.framework.qual.DefaultQualifier;
+
+ @DefaultQualifier(NonNull.class)
+ public final class MappingEnvironment {
++ public static final boolean DISABLE_PLUGIN_REMAPPING = Boolean.getBoolean("paper.disablePluginRemapping");
++ public static final String LEGACY_CB_VERSION = "v1_21_R2";
+ private static final @Nullable String MAPPINGS_HASH = readMappingsHash();
+ private static final boolean REOBF = checkReobf();
+
+diff --git a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
+index 242811578a786e3807a1a7019d472d5a68f87116..0b65fdf53124f3dd042b2363b1b8df8e1ca7de00 100644
+--- a/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
++++ b/src/main/java/io/papermc/paper/util/StacktraceDeobfuscator.java
+@@ -29,6 +29,9 @@ public enum StacktraceDeobfuscator {
+ });
+
+ public void deobfuscateThrowable(final Throwable throwable) {
++ if (!MappingEnvironment.reobf()) {
++ return;
++ }
+ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
+ return;
+ }
+@@ -44,6 +47,9 @@ public enum StacktraceDeobfuscator {
+ }
+
+ public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) {
++ if (!MappingEnvironment.reobf()) {
++ return traceElements;
++ }
+ if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
+ return traceElements;
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+index 7d2d5c4ee244e7118a4c38628e2e69c79b11b98d..371d31266a532e59c49dbb106e354296b119fa5e 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
+@@ -131,36 +131,26 @@ public class Commodore {
+ }
+
+ // Paper start - Plugin rewrites
+- private static final Map<String, String> SEARCH_AND_REMOVE = initReplacementsMap();
+- private static Map<String, String> initReplacementsMap() {
+- Map<String, String> getAndRemove = new HashMap<>();
+- // Be wary of maven shade's relocations
+-
+- final java.util.jar.Manifest manifest = io.papermc.paper.util.JarManifests.manifest(Commodore.class);
+- if (Boolean.getBoolean( "debug.rewriteForIde") && manifest != null)
+- {
+- // unversion incoming calls for pre-relocate debug work
+- final String NMS_REVISION_PACKAGE = "v" + manifest.getMainAttributes().getValue("CraftBukkit-Package-Version") + "/";
+-
+- getAndRemove.put("org/bukkit/".concat("craftbukkit/" + NMS_REVISION_PACKAGE), NMS_REVISION_PACKAGE);
++ private static final String CB_PACKAGE_PREFIX = "org/bukkit/".concat("craftbukkit/");
++ private static final String LEGACY_CB_PACKAGE_PREFIX = CB_PACKAGE_PREFIX + io.papermc.paper.util.MappingEnvironment.LEGACY_CB_VERSION + "/";
++ private static String runtimeCbPkgPrefix() {
++ if (io.papermc.paper.util.MappingEnvironment.reobf()) {
++ return LEGACY_CB_PACKAGE_PREFIX;
+ }
+-
+- return getAndRemove;
++ return CB_PACKAGE_PREFIX;
+ }
+
+ @Nonnull
+ private static String getOriginalOrRewrite(@Nonnull String original)
+ {
+- String rewrite = null;
+- for ( Map.Entry<String, String> entry : SEARCH_AND_REMOVE.entrySet() )
+- {
+- if ( original.contains( entry.getKey() ) )
+- {
+- rewrite = original.replace( entry.getValue(), "" );
++ // Relocation is applied in reobf, and when mappings are present they handle the relocation
++ if (!io.papermc.paper.util.MappingEnvironment.reobf() && !io.papermc.paper.util.MappingEnvironment.hasMappings()) {
++ if (original.contains(LEGACY_CB_PACKAGE_PREFIX)) {
++ original = original.replace(LEGACY_CB_PACKAGE_PREFIX, CB_PACKAGE_PREFIX);
+ }
+ }
+
+- return rewrite != null ? rewrite : original;
++ return original;
+ }
+ // Paper end - Plugin rewrites
+
+@@ -245,6 +235,7 @@ public class Commodore {
+ visitor = new LimitedClassRemapper(cw, new SimpleRemapper(Commodore.ENUM_RENAMES));
+ }
+
++ visitor = io.papermc.paper.pluginremap.reflect.ReflectionRemapper.visitor(visitor); // Paper
+ cr.accept(new ClassRemapper(new ClassVisitor(Opcodes.ASM9, visitor) {
+ final Set<RerouteMethodData> rerouteMethodData = new HashSet<>();
+ String className;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 8e747d48fb76c3f8bacb28f270754d5caeca4445..07316f0043639c608dddadd1b3af871f4c3129d0 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -75,6 +75,7 @@ import org.bukkit.potion.PotionType;
+ @SuppressWarnings("deprecation")
+ public final class CraftMagicNumbers implements UnsafeValues {
+ public static final CraftMagicNumbers INSTANCE = new CraftMagicNumbers();
++ public static final boolean DISABLE_OLD_API_SUPPORT = Boolean.getBoolean("paper.disableOldApiSupport"); // Paper
+
+ private final Commodore commodore = new Commodore();
+
+@@ -347,7 +348,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ throw new InvalidPluginException("Plugin API version " + pdf.getAPIVersion() + " is lower than the minimum allowed version. Please update or replace it.");
+ }
+
+- if (toCheck.isOlderThan(ApiVersion.FLATTENING)) {
++ if (!DISABLE_OLD_API_SUPPORT && toCheck.isOlderThan(ApiVersion.FLATTENING)) { // Paper
+ CraftLegacy.init();
+ }
+
+@@ -362,6 +363,12 @@ public final class CraftMagicNumbers implements UnsafeValues {
+
+ @Override
+ public byte[] processClass(PluginDescriptionFile pdf, String path, byte[] clazz) {
++ // Paper start
++ if (DISABLE_OLD_API_SUPPORT) {
++ // Make sure we still go through our reflection rewriting if needed
++ return io.papermc.paper.pluginremap.reflect.ReflectionRemapper.processClass(clazz);
++ }
++ // Paper end
+ try {
+ clazz = this.commodore.convert(clazz, pdf.getName(), ApiVersion.getOrCreateVersion(pdf.getAPIVersion()), ((CraftServer) Bukkit.getServer()).activeCompatibilities);
+ } catch (Exception ex) {
diff --git a/patches/server/0023-Further-improve-server-tick-loop.patch b/patches/server/0023-Further-improve-server-tick-loop.patch
new file mode 100644
index 0000000000..ff1c5e21de
--- /dev/null
+++ b/patches/server/0023-Further-improve-server-tick-loop.patch
@@ -0,0 +1,233 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Tue, 1 Mar 2016 23:09:29 -0600
+Subject: [PATCH] Further improve server tick loop
+
+Improves how the catchup buffer is handled, allowing it to roll both ways
+increasing the effeciency of the thread sleep so it only will sleep once.
+
+Also increases the buffer of the catchup to ensure server stays at 20 TPS unless extreme conditions
+
+Previous implementation did not calculate TPS correctly.
+Switch to a realistic rolling average and factor in std deviation as an extra reporting variable
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 8acd27005f2b7cf32dc4cf93cf1a6eea2ab594ca..f307408d459196caafc0a485ddb8d1c00bdf0af9 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -307,7 +307,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ public org.bukkit.craftbukkit.CraftServer server;
+ public OptionSet options;
+ public org.bukkit.command.ConsoleCommandSender console;
+- public static int currentTick = (int) (System.currentTimeMillis() / 50);
++ public static int currentTick; // Paper - improve tick loop
+ public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
+ public int autosavePeriod;
+ public Commands vanillaCommandDispatcher;
+@@ -316,7 +316,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ // Spigot start
+ public static final int TPS = 20;
+ public static final int TICK_TIME = 1000000000 / MinecraftServer.TPS;
+- private static final int SAMPLE_INTERVAL = 100;
++ private static final int SAMPLE_INTERVAL = 20; // Paper - improve server tick loop
++ @Deprecated(forRemoval = true) // Paper
+ public final double[] recentTps = new double[ 3 ];
+ // Spigot end
+ public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files
+@@ -1033,6 +1034,57 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ {
+ return ( avg * exp ) + ( tps * ( 1 - exp ) );
+ }
++
++ // Paper start - Further improve server tick loop
++ private static final long SEC_IN_NANO = 1000000000;
++ private static final long MAX_CATCHUP_BUFFER = TICK_TIME * TPS * 60L;
++ private long lastTick = 0;
++ private long catchupTime = 0;
++ public final RollingAverage tps1 = new RollingAverage(60);
++ public final RollingAverage tps5 = new RollingAverage(60 * 5);
++ public final RollingAverage tps15 = new RollingAverage(60 * 15);
++
++ public static class RollingAverage {
++ private final int size;
++ private long time;
++ private java.math.BigDecimal total;
++ private int index = 0;
++ private final java.math.BigDecimal[] samples;
++ private final long[] times;
++
++ RollingAverage(int size) {
++ this.size = size;
++ this.time = size * SEC_IN_NANO;
++ this.total = dec(TPS).multiply(dec(SEC_IN_NANO)).multiply(dec(size));
++ this.samples = new java.math.BigDecimal[size];
++ this.times = new long[size];
++ for (int i = 0; i < size; i++) {
++ this.samples[i] = dec(TPS);
++ this.times[i] = SEC_IN_NANO;
++ }
++ }
++
++ private static java.math.BigDecimal dec(long t) {
++ return new java.math.BigDecimal(t);
++ }
++ public void add(java.math.BigDecimal x, long t) {
++ time -= times[index];
++ total = total.subtract(samples[index].multiply(dec(times[index])));
++ samples[index] = x;
++ times[index] = t;
++ time += t;
++ total = total.add(x.multiply(dec(t)));
++ if (++index == size) {
++ index = 0;
++ }
++ }
++
++ public double getAverage() {
++ return total.divide(dec(time), 30, java.math.RoundingMode.HALF_UP).doubleValue();
++ }
++ }
++ private static final java.math.BigDecimal TPS_BASE = new java.math.BigDecimal(1E9).multiply(new java.math.BigDecimal(SAMPLE_INTERVAL));
++ // Paper end
+ // Spigot End
+
+ protected void runServer() {
+@@ -1047,7 +1099,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ // Spigot start
+ Arrays.fill( this.recentTps, 20 );
+- long tickSection = Util.getMillis(), tickCount = 1;
++ // Paper start - further improve server tick loop
++ long tickSection = Util.getNanos();
++ long currentTime;
++ // Paper end - further improve server tick loop
+ while (this.running) {
+ long i;
+
+@@ -1069,15 +1124,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+ }
+ // Spigot start
+- if ( tickCount++ % MinecraftServer.SAMPLE_INTERVAL == 0 )
+- {
+- long curTime = Util.getMillis();
+- double currentTps = 1E3 / ( curTime - tickSection ) * MinecraftServer.SAMPLE_INTERVAL;
+- this.recentTps[0] = MinecraftServer.calcTps( this.recentTps[0], 0.92, currentTps ); // 1/exp(5sec/1min)
+- this.recentTps[1] = MinecraftServer.calcTps( this.recentTps[1], 0.9835, currentTps ); // 1/exp(5sec/5min)
+- this.recentTps[2] = MinecraftServer.calcTps( this.recentTps[2], 0.9945, currentTps ); // 1/exp(5sec/15min)
+- tickSection = curTime;
++ // Paper start - further improve server tick loop
++ currentTime = Util.getNanos();
++ if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) {
++ final long diff = currentTime - tickSection;
++ final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP);
++ tps1.add(currentTps, diff);
++ tps5.add(currentTps, diff);
++ tps15.add(currentTps, diff);
++
++ // Backwards compat with bad plugins
++ this.recentTps[0] = tps1.getAverage();
++ this.recentTps[1] = tps5.getAverage();
++ this.recentTps[2] = tps15.getAverage();
++ tickSection = currentTime;
+ }
++ // Paper end - further improve server tick loop
+ // Spigot end
+
+ boolean flag = i == 0L;
+@@ -1087,7 +1149,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount);
+ }
+
+- MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit
++ //MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
++ lastTick = currentTime;
+ this.nextTickTimeNanos += i;
+
+ try {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 5c54c5c525c86bb8037982435b8769ec2ca2c6cb..c9920b60a536f5735c61fd7ef154569f6b04c58b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -2680,7 +2680,11 @@ public final class CraftServer implements Server {
+
+ @Override
+ public double[] getTPS() {
+- return new double[]{0, 0, 0}; // TODO
++ return new double[] {
++ net.minecraft.server.MinecraftServer.getServer().tps1.getAverage(),
++ net.minecraft.server.MinecraftServer.getServer().tps5.getAverage(),
++ net.minecraft.server.MinecraftServer.getServer().tps15.getAverage()
++ };
+ }
+
+ // Paper start - adventure sounds
+diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java
+index d9ec48be0fdd2bfea938aa29e36b0f6ffa839ab2..9eb2823cc8f83bad2626fc77578b0162d9ed5782 100644
+--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java
++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java
+@@ -15,6 +15,12 @@ public class TicksPerSecondCommand extends Command
+ this.usageMessage = "/tps";
+ this.setPermission( "bukkit.command.tps" );
+ }
++ // Paper start
++ private static final net.kyori.adventure.text.Component WARN_MSG = net.kyori.adventure.text.Component.text()
++ .append(net.kyori.adventure.text.Component.text("Warning: ", net.kyori.adventure.text.format.NamedTextColor.RED))
++ .append(net.kyori.adventure.text.Component.text("Memory usage on modern garbage collectors is not a stable value and it is perfectly normal to see it reach max. Please do not pay it much attention.", net.kyori.adventure.text.format.NamedTextColor.GOLD))
++ .build();
++ // Paper end
+
+ @Override
+ public boolean execute(CommandSender sender, String currentAlias, String[] args)
+@@ -24,22 +30,40 @@ public class TicksPerSecondCommand extends Command
+ return true;
+ }
+
+- StringBuilder sb = new StringBuilder( ChatColor.GOLD + "TPS from last 1m, 5m, 15m: " );
+- for ( double tps : MinecraftServer.getServer().recentTps )
+- {
+- sb.append( this.format( tps ) );
+- sb.append( ", " );
++ // Paper start - Further improve tick handling
++ double[] tps = org.bukkit.Bukkit.getTPS();
++ net.kyori.adventure.text.Component[] tpsAvg = new net.kyori.adventure.text.Component[tps.length];
++
++ for ( int i = 0; i < tps.length; i++) {
++ tpsAvg[i] = TicksPerSecondCommand.format( tps[i] );
++ }
++
++ net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
++ builder.append(net.kyori.adventure.text.Component.text("TPS from last 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD));
++ builder.append(net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.commas(true), tpsAvg));
++ sender.sendMessage(builder.asComponent());
++ if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) {
++ sender.sendMessage(net.kyori.adventure.text.Component.text()
++ .append(net.kyori.adventure.text.Component.text("Current Memory Usage: ", net.kyori.adventure.text.format.NamedTextColor.GOLD))
++ .append(net.kyori.adventure.text.Component.text(((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: " + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)", net.kyori.adventure.text.format.NamedTextColor.GREEN))
++ );
++ if (!this.hasShownMemoryWarning) {
++ sender.sendMessage(WARN_MSG);
++ this.hasShownMemoryWarning = true;
++ }
+ }
+- sender.sendMessage( sb.substring( 0, sb.length() - 2 ) );
+- sender.sendMessage(ChatColor.GOLD + "Current Memory Usage: " + ChatColor.GREEN + ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: "
+- + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)");
++ // Paper end
+
+ return true;
+ }
+
+- private String format(double tps)
++ private boolean hasShownMemoryWarning; // Paper
++ private static net.kyori.adventure.text.Component format(double tps) // Paper - Made static
+ {
+- return ( ( tps > 18.0 ) ? ChatColor.GREEN : ( tps > 16.0 ) ? ChatColor.YELLOW : ChatColor.RED ).toString()
+- + ( ( tps > 20.0 ) ? "*" : "" ) + Math.min( Math.round( tps * 100.0 ) / 100.0, 20.0 );
++ // Paper
++ net.kyori.adventure.text.format.TextColor color = ( ( tps > 18.0 ) ? net.kyori.adventure.text.format.NamedTextColor.GREEN : ( tps > 16.0 ) ? net.kyori.adventure.text.format.NamedTextColor.YELLOW : net.kyori.adventure.text.format.NamedTextColor.RED );
++ String amount = Math.min(Math.round(tps * 100.0) / 100.0, 20.0) + (tps > 21.0 ? "*" : ""); // Paper - only print * at 21, we commonly peak to 20.02 as the tick sleep is not accurate enough, stop the noise
++ return net.kyori.adventure.text.Component.text(amount, color);
++ // Paper end
+ }
+ }
diff --git a/patches/server/0024-Remove-Spigot-timings.patch b/patches/server/0024-Remove-Spigot-timings.patch
new file mode 100644
index 0000000000..3444309ea5
--- /dev/null
+++ b/patches/server/0024-Remove-Spigot-timings.patch
@@ -0,0 +1,967 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 3 Mar 2016 04:00:11 -0600
+Subject: [PATCH] Remove Spigot timings
+
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index f307408d459196caafc0a485ddb8d1c00bdf0af9..6ae26c5e2a60ee04c786cf19b08678c50c397a6a 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -203,7 +203,6 @@ import org.bukkit.craftbukkit.Main;
+ import org.bukkit.event.server.ServerLoadEvent;
+ // CraftBukkit end
+
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+
+ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
+
+@@ -1456,7 +1455,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+ }
+
+- SpigotTimings.serverTickTimer.startTiming(); // Spigot
+ ++this.tickCount;
+ this.tickRateManager.tick();
+ this.tickChildren(shouldKeepTicking);
+@@ -1472,6 +1470,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
++ this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings)
+ gameprofilerfiller.push("tallying");
+ long k = Util.getNanos() - i;
+ int l = this.tickCount % 100;
+@@ -1482,13 +1481,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float) k / (float) TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F;
+ this.logTickMethodTime(i);
+ gameprofilerfiller.pop();
+- SpigotTimings.serverTickTimer.stopTiming(); // Spigot
+- org.spigotmc.CustomTimingsHandler.tick(); // Spigot
+ }
+
+ private void autoSave() {
+ this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit
+- SpigotTimings.worldSaveTimer.startTiming(); // Spigot
+ MinecraftServer.LOGGER.debug("Autosave started");
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+@@ -1496,7 +1492,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.saveEverything(true, false, false);
+ gameprofilerfiller.pop();
+ MinecraftServer.LOGGER.debug("Autosave finished");
+- SpigotTimings.worldSaveTimer.stopTiming(); // Spigot
+ }
+
+ private void logTickMethodTime(long tickStartTime) {
+@@ -1569,26 +1564,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.getPlayerList().getPlayers().forEach((entityplayer) -> {
+ entityplayer.connection.suspendFlushing();
+ });
+- SpigotTimings.schedulerTimer.startTiming(); // Spigot
+ this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
+- SpigotTimings.schedulerTimer.stopTiming(); // Spigot
+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
+ gameprofilerfiller.push("commandFunctions");
+- SpigotTimings.commandFunctionsTimer.startTiming(); // Spigot
+ this.getFunctions().tick();
+- SpigotTimings.commandFunctionsTimer.stopTiming(); // Spigot
+ gameprofilerfiller.popPush("levels");
+ Iterator iterator = this.getAllLevels().iterator();
+
+ // CraftBukkit start
+ // Run tasks that are waiting on processing
+- SpigotTimings.processQueueTimer.startTiming(); // Spigot
+ while (!this.processQueue.isEmpty()) {
+ this.processQueue.remove().run();
+ }
+- SpigotTimings.processQueueTimer.stopTiming(); // Spigot
+
+- SpigotTimings.timeUpdateTimer.startTiming(); // Spigot
+ // Send time updates to everyone, it will get the right time from the world the player is in.
+ if (this.tickCount % 20 == 0) {
+ for (int i = 0; i < this.getPlayerList().players.size(); ++i) {
+@@ -1596,7 +1584,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ entityplayer.connection.send(new ClientboundSetTimePacket(entityplayer.level().getGameTime(), entityplayer.getPlayerTime(), entityplayer.serverLevel().getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); // Add support for per player time
+ }
+ }
+- SpigotTimings.timeUpdateTimer.stopTiming(); // Spigot
+
+ while (iterator.hasNext()) {
+ ServerLevel worldserver = (ServerLevel) iterator.next();
+@@ -1617,9 +1604,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ gameprofilerfiller.push("tick");
+
+ try {
+- worldserver.timings.doTick.startTiming(); // Spigot
+ worldserver.tick(shouldKeepTicking);
+- worldserver.timings.doTick.stopTiming(); // Spigot
+ } catch (Throwable throwable) {
+ CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world");
+
+@@ -1632,24 +1617,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+
+ gameprofilerfiller.popPush("connection");
+- SpigotTimings.connectionTimer.startTiming(); // Spigot
+ this.tickConnection();
+- SpigotTimings.connectionTimer.stopTiming(); // Spigot
+ gameprofilerfiller.popPush("players");
+- SpigotTimings.playerListTimer.startTiming(); // Spigot
+ this.playerList.tick();
+- SpigotTimings.playerListTimer.stopTiming(); // Spigot
+ if (SharedConstants.IS_RUNNING_IN_IDE && this.tickRateManager.runsNormally()) {
+ GameTestTicker.SINGLETON.tick();
+ }
+
+ gameprofilerfiller.popPush("server gui refresh");
+
+- SpigotTimings.tickablesTimer.startTiming(); // Spigot
+ for (int i = 0; i < this.tickables.size(); ++i) {
+ ((Runnable) this.tickables.get(i)).run();
+ }
+- SpigotTimings.tickablesTimer.stopTiming(); // Spigot
+
+ gameprofilerfiller.popPush("send chunks");
+ iterator = this.playerList.getPlayers().iterator();
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index 9d6be455c3bbcdbcb9d3d24b0bad79f46ba6a8cb..a129ddfe7b00d6abab94437806a5cfb9668e7cc9 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -64,7 +64,6 @@ import org.apache.logging.log4j.Level;
+ import org.apache.logging.log4j.LogManager;
+ import org.apache.logging.log4j.io.IoBuilder;
+ import org.bukkit.command.CommandSender;
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+ import org.bukkit.craftbukkit.util.TerminalCompletionHandler;
+ import org.bukkit.craftbukkit.util.TerminalConsoleWriterThread;
+ import org.bukkit.event.server.ServerCommandEvent;
+@@ -421,7 +420,6 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ }
+
+ public void handleConsoleInputs() {
+- SpigotTimings.serverCommandTimer.startTiming(); // Spigot
+ while (!this.consoleInput.isEmpty()) {
+ ConsoleInput servercommand = (ConsoleInput) this.consoleInput.remove(0);
+
+@@ -436,7 +434,6 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ // CraftBukkit end
+ }
+
+- SpigotTimings.serverCommandTimer.stopTiming(); // Spigot
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 3e35a64b4b92ec25789e85c7445375dd899e1805..2e2976efcf99de269f67dec2c87cb910ff280562 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -210,13 +210,11 @@ public class ServerChunkCache extends ChunkSource {
+ }
+
+ gameprofilerfiller.incrementCounter("getChunkCacheMiss");
+- this.level.timings.syncChunkLoadTimer.startTiming(); // Spigot
+ CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
+ ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
+
+ Objects.requireNonNull(completablefuture);
+ chunkproviderserver_b.managedBlock(completablefuture::isDone);
+- this.level.timings.syncChunkLoadTimer.stopTiming(); // Spigot
+ ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
+ ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
+
+@@ -417,25 +415,19 @@ public class ServerChunkCache extends ChunkSource {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("purge");
+- this.level.timings.doChunkMap.startTiming(); // Spigot
+ if (this.level.tickRateManager().runsNormally() || !tickChunks || this.level.spigotConfig.unloadFrozenChunks) { // Spigot
+ this.distanceManager.purgeStaleTickets();
+ }
+
+ this.runDistanceManagerUpdates();
+- this.level.timings.doChunkMap.stopTiming(); // Spigot
+ gameprofilerfiller.popPush("chunks");
+ if (tickChunks) {
+ this.tickChunks();
+- this.level.timings.tracker.startTiming(); // Spigot
+ this.chunkMap.tick();
+- this.level.timings.tracker.stopTiming(); // Spigot
+ }
+
+- this.level.timings.doChunkUnload.startTiming(); // Spigot
+ gameprofilerfiller.popPush("unload");
+ this.chunkMap.tick(shouldKeepTicking);
+- this.level.timings.doChunkUnload.stopTiming(); // Spigot
+ gameprofilerfiller.pop();
+ this.clearCache();
+ }
+@@ -528,9 +520,7 @@ public class ServerChunkCache extends ChunkSource {
+ }
+
+ if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
+- this.level.timings.doTickTiles.startTiming(); // Spigot
+ this.level.tickChunk(chunk, k);
+- this.level.timings.doTickTiles.stopTiming(); // Spigot
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index cba44ea8375692ce9d2511fba1ac1dd1d2d0cb1e..9415c699ed2bc2b4237ab5e14cb8316410ac9fa5 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -177,7 +177,6 @@ import net.minecraft.world.ticks.LevelTicks;
+ import org.slf4j.Logger;
+ import org.bukkit.Bukkit;
+ import org.bukkit.WeatherType;
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+ import org.bukkit.craftbukkit.event.CraftEventFactory;
+ import org.bukkit.craftbukkit.generator.CustomWorldChunkManager;
+ import org.bukkit.craftbukkit.util.WorldUUID;
+@@ -475,7 +474,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ }
+
+ gameprofilerfiller.push("tickPending");
+- this.timings.doTickPending.startTiming(); // Spigot
+ if (!this.isDebug() && flag) {
+ j = this.getGameTime();
+ gameprofilerfiller.push("blockTicks");
+@@ -484,7 +482,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ this.fluidTicks.tick(j, 65536, this::tickFluid);
+ gameprofilerfiller.pop();
+ }
+- this.timings.doTickPending.stopTiming(); // Spigot
+
+ gameprofilerfiller.popPush("raid");
+ if (flag) {
+@@ -495,9 +492,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ this.getChunkSource().tick(shouldKeepTicking, true);
+ gameprofilerfiller.popPush("blockEvents");
+ if (flag) {
+- this.timings.doSounds.startTiming(); // Spigot
+ this.runBlockEvents();
+- this.timings.doSounds.stopTiming(); // Spigot
+ }
+
+ this.handlingTick = false;
+@@ -510,7 +505,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ if (flag1 || this.emptyTime++ < 300) {
+ gameprofilerfiller.push("entities");
+- this.timings.tickEntities.startTiming(); // Spigot
+ if (this.dragonFight != null && flag) {
+ gameprofilerfiller.push("dragonFight");
+ this.dragonFight.tick();
+@@ -518,7 +512,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ }
+
+ org.spigotmc.ActivationRange.activateEntities(this); // Spigot
+- this.timings.entityTick.startTiming(); // Spigot
+ this.entityTickList.forEach((entity) -> {
+ if (!entity.isRemoved()) {
+ if (!tickratemanager.isEntityFrozen(entity)) {
+@@ -543,8 +536,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ }
+ }
+ });
+- this.timings.entityTick.stopTiming(); // Spigot
+- this.timings.tickEntities.stopTiming(); // Spigot
+ gameprofilerfiller.pop();
+ this.tickBlockEntities();
+ }
+@@ -957,7 +948,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return;
+ }
+ // Spigot end
+- entity.tickTimer.startTiming(); // Spigot
+ entity.setOldPosAndRot();
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+@@ -976,7 +966,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ this.tickPassenger(entity, entity1);
+ }
+- entity.tickTimer.stopTiming(); // Spigot
+
+ }
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index ef98348a701efb10d65414f9ab2acd640900d24f..f21de202d7c932832fca9402a17a13e336aa36c8 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -341,7 +341,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+
+ @Override
+ public void tick() {
+- org.bukkit.craftbukkit.SpigotTimings.playerConnectionTimer.startTiming(); // Spigot
+ if (this.ackBlockChangesUpTo > -1) {
+ this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
+ this.ackBlockChangesUpTo = -1;
+@@ -397,7 +396,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854
+ this.disconnect((Component) Component.translatable("multiplayer.disconnect.idling"));
+ }
+- org.bukkit.craftbukkit.SpigotTimings.playerConnectionTimer.stopTiming(); // Spigot
+
+ }
+
+@@ -2189,7 +2187,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ private void handleCommand(String s) {
+- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.startTiming(); // Spigot
+ if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot
+ this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s);
+
+@@ -2199,7 +2196,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ this.cserver.getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.stopTiming(); // Spigot
+ return;
+ }
+
+@@ -2212,7 +2208,6 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
+ return;
+ } finally {
+- org.bukkit.craftbukkit.SpigotTimings.playerCommandTimer.stopTiming(); // Spigot
+ }
+ }
+ // CraftBukkit end
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index cbbed9eff4c5fa5bcb67efd73cb15c539aa286b9..fdeb762077bf0b87ceb62697f935f611eb9b046b 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -149,7 +149,6 @@ import org.bukkit.command.CommandSender;
+ import org.bukkit.entity.Hanging;
+ import org.bukkit.entity.LivingEntity;
+ import org.bukkit.entity.Vehicle;
+-import org.spigotmc.CustomTimingsHandler; // Spigot
+ import org.bukkit.event.entity.EntityCombustByEntityEvent;
+ import org.bukkit.event.hanging.HangingBreakByEntityEvent;
+ import org.bukkit.event.vehicle.VehicleBlockCollisionEvent;
+@@ -327,7 +326,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ // Marks an entity, that it was removed by a plugin via Entity#remove
+ // Main use case currently is for SPIGOT-7487, preventing dropping of leash when leash is removed
+ public boolean pluginRemoved = false;
+- public CustomTimingsHandler tickTimer = org.bukkit.craftbukkit.SpigotTimings.getEntityTimings(this); // Spigot
+ // Spigot start
+ public final org.spigotmc.ActivationRange.ActivationType activationType = org.spigotmc.ActivationRange.initializeEntityActivationType(this);
+ public final boolean defaultActivationState;
+@@ -875,7 +873,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ }
+
+ public void move(MoverType type, Vec3 movement) {
+- org.bukkit.craftbukkit.SpigotTimings.entityMoveTimer.startTiming(); // Spigot
+ if (this.noPhysics) {
+ this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
+ } else {
+@@ -990,7 +987,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ gameprofilerfiller.pop();
+ }
+ }
+- org.bukkit.craftbukkit.SpigotTimings.entityMoveTimer.stopTiming(); // Spigot
+ }
+
+ private void applyMovementEmissionAndPlaySound(Entity.MovementEmission moveEffect, Vec3 movement, BlockPos landingPos, BlockState landingState) {
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index d679363fb85e08c57e2886a24b88ee0a82afcf34..25d2aa773f67946bf18312975c5b25f93015c39c 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -161,8 +161,6 @@ import org.bukkit.event.entity.EntityTeleportEvent;
+ import org.bukkit.event.player.PlayerItemConsumeEvent;
+ // CraftBukkit end
+
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+-
+ public abstract class LivingEntity extends Entity implements Attackable {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+@@ -3102,7 +3100,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ @Override
+ public void tick() {
+- SpigotTimings.timerEntityBaseTick.startTiming(); // Spigot
+ super.tick();
+ this.updatingUsingItem();
+ this.updateSwimAmount();
+@@ -3144,9 +3141,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ if (!this.isRemoved()) {
+- SpigotTimings.timerEntityBaseTick.stopTiming(); // Spigot
+ this.aiStep();
+- SpigotTimings.timerEntityTickRest.startTiming(); // Spigot
+ }
+
+ double d0 = this.getX() - this.xo;
+@@ -3240,7 +3235,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ this.elytraAnimationState.tick();
+- SpigotTimings.timerEntityTickRest.stopTiming(); // Spigot
+ }
+
+ public void detectEquipmentUpdatesPublic() { // CraftBukkit
+@@ -3447,7 +3441,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("ai");
+- SpigotTimings.timerEntityAI.startTiming(); // Spigot
+ if (this.isImmobile()) {
+ this.jumping = false;
+ this.xxa = 0.0F;
+@@ -3457,7 +3450,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ this.serverAiStep();
+ gameprofilerfiller.pop();
+ }
+- SpigotTimings.timerEntityAI.stopTiming(); // Spigot
+
+ gameprofilerfiller.pop();
+ gameprofilerfiller.push("jump");
+@@ -3500,7 +3492,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ this.resetFallDistance();
+ }
+
+- SpigotTimings.timerEntityAIMove.startTiming(); // Spigot
+ label112:
+ {
+ LivingEntity entityliving = this.getControllingPassenger();
+@@ -3514,7 +3505,6 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ this.travel(vec3d1);
+ }
+- SpigotTimings.timerEntityAIMove.stopTiming(); // Spigot
+
+ if (!this.level().isClientSide() || this.isControlledByLocalInstance()) {
+ this.applyEffectsFromBlocks();
+@@ -3550,9 +3540,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ this.checkAutoSpinAttack(axisalignedbb, this.getBoundingBox());
+ }
+
+- SpigotTimings.timerEntityAICollision.startTiming(); // Spigot
+ this.pushEntities();
+- SpigotTimings.timerEntityAICollision.stopTiming(); // Spigot
+ gameprofilerfiller.pop();
+ world = this.level();
+ if (world instanceof ServerLevel worldserver) {
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index a3fd246b6a09a77fa64ef8e435edadf77dfbb1d7..0aa5d3c85b021c552eda139850b48effc3613450 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -96,7 +96,6 @@ import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePa
+ import org.bukkit.Bukkit;
+ import org.bukkit.craftbukkit.CraftServer;
+ import org.bukkit.craftbukkit.CraftWorld;
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+ import org.bukkit.craftbukkit.block.CapturedBlockState;
+ import org.bukkit.craftbukkit.block.CraftBlockState;
+ import org.bukkit.craftbukkit.block.data.CraftBlockData;
+@@ -166,7 +165,6 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ }
+ // Paper end - add paper world config
+
+- public final SpigotTimings.WorldTimingsHandler timings; // Spigot
+ public static BlockPos lastPhysicsProblem; // Spigot
+ private org.spigotmc.TickLimiter entityLimiter;
+ private org.spigotmc.TickLimiter tileLimiter;
+@@ -260,7 +258,6 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ public void onBorderSetDamageSafeZOne(WorldBorder border, double safeZoneRadius) {}
+ });
+ // CraftBukkit end
+- this.timings = new SpigotTimings.WorldTimingsHandler(this); // Spigot - code below can generate new world and access timings
+ this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
+ this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
+ }
+@@ -693,15 +690,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("blockEntities");
+- this.timings.tileEntityPending.startTiming(); // Spigot
+ this.tickingBlockEntities = true;
+ if (!this.pendingBlockEntityTickers.isEmpty()) {
+ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
+ this.pendingBlockEntityTickers.clear();
+ }
+- this.timings.tileEntityPending.stopTiming(); // Spigot
+
+- this.timings.tileEntityTick.startTiming(); // Spigot
+ // Spigot start
+ // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
+ boolean flag = this.tickRateManager().runsNormally();
+@@ -724,7 +718,6 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ }
+ }
+
+- this.timings.tileEntityTick.stopTiming(); // Spigot
+ this.tickingBlockEntities = false;
+ gameprofilerfiller.pop();
+ this.spigotConfig.currentPrimedTnt = 0; // Spigot
+@@ -732,9 +725,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+
+ public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
+ try {
+- SpigotTimings.tickEntityTimer.startTiming(); // Spigot
+ tickConsumer.accept(entity);
+- SpigotTimings.tickEntityTimer.stopTiming(); // Spigot
+ } catch (Throwable throwable) {
+ CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking entity");
+ CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being ticked");
+diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+index 1fe93e01c5e37397aded5d1f99214bf1bffe70b7..9389fd53f2bff0a9ca389694b312dc6da58befaf 100644
+--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+@@ -148,7 +148,6 @@ public final class NaturalSpawner {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("spawner");
+- world.timings.mobSpawn.startTiming(); // Spigot
+ Iterator iterator = spawnableGroups.iterator();
+
+ while (iterator.hasNext()) {
+@@ -163,7 +162,6 @@ public final class NaturalSpawner {
+ }
+ }
+
+- world.timings.mobSpawn.stopTiming(); // Spigot
+ gameprofilerfiller.pop();
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+index 33b0030ceae7dae2958ec0275613b7886fbedbc1..1fa90dbf0e431d1f69ab46aa3dc200f09cfe7536 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -32,11 +32,8 @@ import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
+ import org.bukkit.inventory.InventoryHolder;
+ // CraftBukkit end
+
+-import org.spigotmc.CustomTimingsHandler; // Spigot
+-
+ public abstract class BlockEntity {
+
+- public CustomTimingsHandler tickTimer = org.bukkit.craftbukkit.SpigotTimings.getTileEntityTimings(this); // Spigot
+ // CraftBukkit start - data containers
+ private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
+ public CraftPersistentDataContainer persistentDataContainer;
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index 9e6889d20bc2c9e86103f6d935d344de3ec48050..b215ed941197f5aa0e26cd50a420500dcdc17fbb 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -976,7 +976,6 @@ public class LevelChunk extends ChunkAccess {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push(this::getType);
+- this.blockEntity.tickTimer.startTiming(); // Spigot
+ BlockState iblockdata = LevelChunk.this.getBlockState(blockposition);
+
+ if (this.blockEntity.getType().isValid(iblockdata)) {
+@@ -995,9 +994,6 @@ public class LevelChunk extends ChunkAccess {
+ this.blockEntity.fillCrashReportCategory(crashreportsystemdetails);
+ throw new ReportedException(crashreport);
+ // Spigot start
+- } finally {
+- this.blockEntity.tickTimer.stopTiming();
+- // Spigot end
+ }
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+index d1b82dec25069a7027aaf53086b1829e511fc301..4367ccc628bb4f404d6a081083002518442f462b 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+@@ -576,15 +576,12 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
+ @Nullable
+ private static LevelChunk.PostLoadProcessor postLoadChunk(ServerLevel world, List<CompoundTag> entities, List<CompoundTag> blockEntities) {
+ return entities.isEmpty() && blockEntities.isEmpty() ? null : (chunk) -> {
+- world.timings.syncChunkLoadEntitiesTimer.startTiming(); // Spigot
+ if (!entities.isEmpty()) {
+ world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD));
+ }
+- world.timings.syncChunkLoadEntitiesTimer.stopTiming(); // Spigot
+
+ Iterator iterator = blockEntities.iterator();
+
+- world.timings.syncChunkLoadTileEntitiesTimer.startTiming(); // Spigot
+ while (iterator.hasNext()) {
+ CompoundTag nbttagcompound = (CompoundTag) iterator.next();
+ boolean flag = nbttagcompound.getBoolean("keepPacked");
+@@ -600,7 +597,6 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
+ }
+ }
+ }
+- world.timings.syncChunkLoadTileEntitiesTimer.stopTiming(); // Spigot
+
+ };
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index c9920b60a536f5735c61fd7ef154569f6b04c58b..4d80804c0992d8fe90526b9f3ce858ec710e7361 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -379,7 +379,6 @@ public final class CraftServer implements Server {
+ this.saveCommandsConfig();
+ this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*");
+ this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions");
+- this.pluginManager.useTimings(this.configuration.getBoolean("settings.plugin-profiling"));
+ this.overrideSpawnLimits();
+ console.autosavePeriod = this.configuration.getInt("ticks-per.autosave");
+ this.warningState = WarningState.value(this.configuration.getString("settings.deprecated-verbose"));
+@@ -2646,12 +2645,31 @@ public final class CraftServer implements Server {
+ private final org.bukkit.Server.Spigot spigot = new org.bukkit.Server.Spigot()
+ {
+
++ @Deprecated
+ @Override
+ public YamlConfiguration getConfig()
+ {
+ return org.spigotmc.SpigotConfig.config;
+ }
+
++ @Override
++ public YamlConfiguration getBukkitConfig()
++ {
++ return configuration;
++ }
++
++ @Override
++ public YamlConfiguration getSpigotConfig()
++ {
++ return org.spigotmc.SpigotConfig.config;
++ }
++
++ @Override
++ public YamlConfiguration getPaperConfig()
++ {
++ return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console);
++ }
++
+ @Override
+ public void restart() {
+ org.spigotmc.RestartCommand.restart();
+diff --git a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java b/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java
+deleted file mode 100644
+index b0ffa23faf62629043dfd613315eaf9c5fcc2cfe..0000000000000000000000000000000000000000
+--- a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java
++++ /dev/null
+@@ -1,163 +0,0 @@
+-package org.bukkit.craftbukkit;
+-
+-import java.util.HashMap;
+-import net.minecraft.world.entity.Entity;
+-import net.minecraft.world.level.Level;
+-import net.minecraft.world.level.block.entity.BlockEntity;
+-import net.minecraft.world.level.storage.PrimaryLevelData;
+-import org.bukkit.craftbukkit.scheduler.CraftTask;
+-import org.bukkit.plugin.java.JavaPluginLoader;
+-import org.bukkit.scheduler.BukkitTask;
+-import org.spigotmc.CustomTimingsHandler;
+-
+-public class SpigotTimings {
+-
+- public static final CustomTimingsHandler serverTickTimer = new CustomTimingsHandler("** Full Server Tick");
+- public static final CustomTimingsHandler playerListTimer = new CustomTimingsHandler("Player List");
+- public static final CustomTimingsHandler commandFunctionsTimer = new CustomTimingsHandler("Command Functions");
+- public static final CustomTimingsHandler connectionTimer = new CustomTimingsHandler("Connection Handler");
+- public static final CustomTimingsHandler playerConnectionTimer = new CustomTimingsHandler("** PlayerConnection");
+- public static final CustomTimingsHandler tickablesTimer = new CustomTimingsHandler("Tickables");
+- public static final CustomTimingsHandler schedulerTimer = new CustomTimingsHandler("Scheduler");
+- public static final CustomTimingsHandler timeUpdateTimer = new CustomTimingsHandler("Time Update");
+- public static final CustomTimingsHandler serverCommandTimer = new CustomTimingsHandler("Server Command");
+- public static final CustomTimingsHandler worldSaveTimer = new CustomTimingsHandler("World Save");
+-
+- public static final CustomTimingsHandler entityMoveTimer = new CustomTimingsHandler("** entityMove");
+- public static final CustomTimingsHandler tickEntityTimer = new CustomTimingsHandler("** tickEntity");
+- public static final CustomTimingsHandler activatedEntityTimer = new CustomTimingsHandler("** activatedTickEntity");
+- public static final CustomTimingsHandler tickTileEntityTimer = new CustomTimingsHandler("** tickTileEntity");
+-
+- public static final CustomTimingsHandler timerEntityBaseTick = new CustomTimingsHandler("** livingEntityBaseTick");
+- public static final CustomTimingsHandler timerEntityAI = new CustomTimingsHandler("** livingEntityAI");
+- public static final CustomTimingsHandler timerEntityAICollision = new CustomTimingsHandler("** livingEntityAICollision");
+- public static final CustomTimingsHandler timerEntityAIMove = new CustomTimingsHandler("** livingEntityAIMove");
+- public static final CustomTimingsHandler timerEntityTickRest = new CustomTimingsHandler("** livingEntityTickRest");
+-
+- public static final CustomTimingsHandler processQueueTimer = new CustomTimingsHandler("processQueue");
+- public static final CustomTimingsHandler schedulerSyncTimer = new CustomTimingsHandler("** Scheduler - Sync Tasks", JavaPluginLoader.pluginParentTimer);
+-
+- public static final CustomTimingsHandler playerCommandTimer = new CustomTimingsHandler("** playerCommand");
+-
+- public static final CustomTimingsHandler entityActivationCheckTimer = new CustomTimingsHandler("entityActivationCheck");
+- public static final CustomTimingsHandler checkIfActiveTimer = new CustomTimingsHandler("** checkIfActive");
+-
+- public static final HashMap<String, CustomTimingsHandler> entityTypeTimingMap = new HashMap<String, CustomTimingsHandler>();
+- public static final HashMap<String, CustomTimingsHandler> tileEntityTypeTimingMap = new HashMap<String, CustomTimingsHandler>();
+- public static final HashMap<String, CustomTimingsHandler> pluginTaskTimingMap = new HashMap<String, CustomTimingsHandler>();
+-
+- /**
+- * Gets a timer associated with a plugins tasks.
+- * @param task
+- * @param period
+- * @return
+- */
+- public static CustomTimingsHandler getPluginTaskTimings(BukkitTask task, long period) {
+- if (!task.isSync()) {
+- return null;
+- }
+- String plugin;
+- final CraftTask ctask = (CraftTask) task;
+-
+- if (task.getOwner() != null) {
+- plugin = task.getOwner().getDescription().getFullName();
+- } else {
+- plugin = "Unknown";
+- }
+- String taskname = ctask.getTaskName();
+-
+- String name = "Task: " + plugin + " Runnable: " + taskname;
+- if (period > 0) {
+- name += "(interval:" + period + ")";
+- } else {
+- name += "(Single)";
+- }
+- CustomTimingsHandler result = SpigotTimings.pluginTaskTimingMap.get(name);
+- if (result == null) {
+- result = new CustomTimingsHandler(name, SpigotTimings.schedulerSyncTimer);
+- SpigotTimings.pluginTaskTimingMap.put(name, result);
+- }
+- return result;
+- }
+-
+- /**
+- * Get a named timer for the specified entity type to track type specific timings.
+- * @param entity
+- * @return
+- */
+- public static CustomTimingsHandler getEntityTimings(Entity entity) {
+- String entityType = entity.getClass().getName();
+- CustomTimingsHandler result = SpigotTimings.entityTypeTimingMap.get(entityType);
+- if (result == null) {
+- result = new CustomTimingsHandler("** tickEntity - " + entity.getClass().getSimpleName(), SpigotTimings.activatedEntityTimer);
+- SpigotTimings.entityTypeTimingMap.put(entityType, result);
+- }
+- return result;
+- }
+-
+- /**
+- * Get a named timer for the specified tile entity type to track type specific timings.
+- * @param entity
+- * @return
+- */
+- public static CustomTimingsHandler getTileEntityTimings(BlockEntity entity) {
+- String entityType = entity.getClass().getName();
+- CustomTimingsHandler result = SpigotTimings.tileEntityTypeTimingMap.get(entityType);
+- if (result == null) {
+- result = new CustomTimingsHandler("** tickTileEntity - " + entity.getClass().getSimpleName(), SpigotTimings.tickTileEntityTimer);
+- SpigotTimings.tileEntityTypeTimingMap.put(entityType, result);
+- }
+- return result;
+- }
+-
+- /**
+- * Set of timers per world, to track world specific timings.
+- */
+- public static class WorldTimingsHandler {
+- public final CustomTimingsHandler mobSpawn;
+- public final CustomTimingsHandler doChunkUnload;
+- public final CustomTimingsHandler doTickPending;
+- public final CustomTimingsHandler doTickTiles;
+- public final CustomTimingsHandler doChunkMap;
+- public final CustomTimingsHandler doSounds;
+- public final CustomTimingsHandler entityTick;
+- public final CustomTimingsHandler tileEntityTick;
+- public final CustomTimingsHandler tileEntityPending;
+- public final CustomTimingsHandler tracker;
+- public final CustomTimingsHandler doTick;
+- public final CustomTimingsHandler tickEntities;
+-
+- public final CustomTimingsHandler syncChunkLoadTimer;
+- public final CustomTimingsHandler syncChunkLoadStructuresTimer;
+- public final CustomTimingsHandler syncChunkLoadEntitiesTimer;
+- public final CustomTimingsHandler syncChunkLoadTileEntitiesTimer;
+- public final CustomTimingsHandler syncChunkLoadTileTicksTimer;
+- public final CustomTimingsHandler syncChunkLoadPostTimer;
+-
+- public WorldTimingsHandler(Level server) {
+- String name = ((PrimaryLevelData) server.levelData).getLevelName() + " - ";
+-
+- this.mobSpawn = new CustomTimingsHandler("** " + name + "mobSpawn");
+- this.doChunkUnload = new CustomTimingsHandler("** " + name + "doChunkUnload");
+- this.doTickPending = new CustomTimingsHandler("** " + name + "doTickPending");
+- this.doTickTiles = new CustomTimingsHandler("** " + name + "doTickTiles");
+- this.doChunkMap = new CustomTimingsHandler("** " + name + "doChunkMap");
+- this.doSounds = new CustomTimingsHandler("** " + name + "doSounds");
+- this.entityTick = new CustomTimingsHandler("** " + name + "entityTick");
+- this.tileEntityTick = new CustomTimingsHandler("** " + name + "tileEntityTick");
+- this.tileEntityPending = new CustomTimingsHandler("** " + name + "tileEntityPending");
+-
+- this.syncChunkLoadTimer = new CustomTimingsHandler("** " + name + "syncChunkLoad");
+- this.syncChunkLoadStructuresTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Structures");
+- this.syncChunkLoadEntitiesTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Entities");
+- this.syncChunkLoadTileEntitiesTimer = new CustomTimingsHandler("** " + name + "chunkLoad - TileEntities");
+- this.syncChunkLoadTileTicksTimer = new CustomTimingsHandler("** " + name + "chunkLoad - TileTicks");
+- this.syncChunkLoadPostTimer = new CustomTimingsHandler("** " + name + "chunkLoad - Post");
+-
+-
+- this.tracker = new CustomTimingsHandler(name + "tracker");
+- this.doTick = new CustomTimingsHandler(name + "doTick");
+- this.tickEntities = new CustomTimingsHandler(name + "tickEntities");
+- }
+- }
+-}
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 6172bce93681e94b4cb19f7164f739e599108e00..a37e5d822e93b3a4b1c0becf22290017c4709b94 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2794,6 +2794,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ CraftPlayer.this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(components, position == net.md_5.bungee.api.ChatMessageType.ACTION_BAR));
+ }
++
++ // Paper start
++ @Override
++ public int getPing()
++ {
++ return CraftPlayer.this.getPing();
++ }
++ // Paper end
+ };
+
+ public Player.Spigot spigot()
+diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+index a7b53187a24d11b8c91e8c50eeb907aca60891cb..0385aa1e5cced22bbafdabca1b63599db1f5d3f6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+@@ -413,9 +413,7 @@ public class CraftScheduler implements BukkitScheduler {
+ if (task.isSync()) {
+ this.currentTask = task;
+ try {
+- task.timings.startTiming(); // Spigot
+ task.run();
+- task.timings.stopTiming(); // Spigot
+ } catch (final Throwable throwable) {
+ task.getOwner().getLogger().log(
+ Level.WARNING,
+diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
+index e4d1eb4a0ce2c9874922585f6bb0d9ead433fde1..17680f112d0c7e7aee07e34477daa21ef2ddaa6f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
+@@ -1,13 +1,11 @@
+ package org.bukkit.craftbukkit.scheduler;
+
+ import java.util.function.Consumer;
++
+ import org.bukkit.Bukkit;
+ import org.bukkit.plugin.Plugin;
+ import org.bukkit.scheduler.BukkitTask;
+
+-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+-import org.spigotmc.CustomTimingsHandler; // Spigot
+-
+ public class CraftTask implements BukkitTask, Runnable { // Spigot
+
+ private volatile CraftTask next = null;
+@@ -26,13 +24,12 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot
+ */
+ private volatile long period;
+ private long nextRun;
+- private final Runnable rTask;
+- private final Consumer<BukkitTask> cTask;
++ public final Runnable rTask;
++ public final Consumer<BukkitTask> cTask;
+ private final Plugin plugin;
+ private final int id;
+ private final long createdAt = System.nanoTime();
+
+- final CustomTimingsHandler timings; // Spigot
+ CraftTask() {
+ this(null, null, CraftTask.NO_REPEATING, CraftTask.NO_REPEATING);
+ }
+@@ -58,7 +55,6 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot
+ }
+ this.id = id;
+ this.period = period;
+- this.timings = this.isSync() ? SpigotTimings.getPluginTaskTimings(this, period) : null; // Spigot
+ }
+
+ @Override
+@@ -137,9 +133,4 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot
+ return true;
+ }
+
+- // Spigot start
+- public String getTaskName() {
+- return (this.getTaskClass() == null) ? "Unknown" : this.getTaskClass().getName();
+- }
+- // Spigot end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java b/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java
+index f97eccb6a17c7876e1e002d798eb67bbe80571a0..dba31a2cbcfebe1f28883545ce4a70fcb9251aa6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftIconCache.java
+@@ -1,6 +1,7 @@
+ package org.bukkit.craftbukkit.util;
+
+ import org.bukkit.util.CachedServerIcon;
++import org.jetbrains.annotations.Nullable;
+
+ public class CraftIconCache implements CachedServerIcon {
+ public final byte[] value;
+@@ -8,4 +9,12 @@ public class CraftIconCache implements CachedServerIcon {
+ public CraftIconCache(final byte[] value) {
+ this.value = value;
+ }
++
++ @Override
++ public @Nullable String getData() {
++ if (this.value == null) {
++ return null;
++ }
++ return "data:image/png;base64," + new String(java.util.Base64.getEncoder().encode(this.value), java.nio.charset.StandardCharsets.UTF_8);
++ }
+ }
+diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
+index 5baf68732cb0e5ecab9d809df54a42e8252f1624..0338ceaddc1a0921f5f8796d5eac75c301bafac2 100644
+--- a/src/main/java/org/spigotmc/ActivationRange.java
++++ b/src/main/java/org/spigotmc/ActivationRange.java
+@@ -28,7 +28,6 @@ import net.minecraft.world.entity.projectile.ThrownTrident;
+ import net.minecraft.world.entity.raid.Raider;
+ import net.minecraft.world.level.Level;
+ import net.minecraft.world.phys.AABB;
+-import org.bukkit.craftbukkit.SpigotTimings;
+
+ public class ActivationRange
+ {
+@@ -111,7 +110,6 @@ public class ActivationRange
+ */
+ public static void activateEntities(Level world)
+ {
+- SpigotTimings.entityActivationCheckTimer.startTiming();
+ final int miscActivationRange = world.spigotConfig.miscActivationRange;
+ final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
+ final int animalActivationRange = world.spigotConfig.animalActivationRange;
+@@ -138,7 +136,6 @@ public class ActivationRange
+
+ world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity);
+ }
+- SpigotTimings.entityActivationCheckTimer.stopTiming();
+ }
+
+ /**
+@@ -233,10 +230,8 @@ public class ActivationRange
+ */
+ public static boolean checkIfActive(Entity entity)
+ {
+- SpigotTimings.checkIfActiveTimer.startTiming();
+ // Never safe to skip fireworks or item gravity
+ if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId() + 1) % 4 == 0)) {
+- SpigotTimings.checkIfActiveTimer.stopTiming();
+ return true;
+ }
+
+@@ -260,7 +255,6 @@ public class ActivationRange
+ {
+ isActive = false;
+ }
+- SpigotTimings.checkIfActiveTimer.stopTiming();
+ return isActive;
+ }
+ }
diff --git a/patches/server/0025-Add-command-line-option-to-load-extra-plugin-jars-no.patch b/patches/server/0025-Add-command-line-option-to-load-extra-plugin-jars-no.patch
new file mode 100644
index 0000000000..9ba347c792
--- /dev/null
+++ b/patches/server/0025-Add-command-line-option-to-load-extra-plugin-jars-no.patch
@@ -0,0 +1,65 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <[email protected]>
+Date: Tue, 18 May 2021 14:39:44 -0700
+Subject: [PATCH] Add command line option to load extra plugin jars not in the
+ plugins folder
+
+ex: java -jar paperclip.jar nogui -add-plugin=/path/to/plugin.jar -add-plugin=/path/to/another/plugin_jar.jar
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 4d80804c0992d8fe90526b9f3ce858ec710e7361..16d2b3e59b8a6ef65b411afb9d94c61e6d797e36 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -461,6 +461,35 @@ public final class CraftServer implements Server {
+ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
+ }
+
++ // Paper start
++ @Override
++ public File getPluginsFolder() {
++ return this.console.getPluginsFolder();
++ }
++
++ private List<File> extraPluginJars() {
++ @SuppressWarnings("unchecked")
++ final List<File> jars = (List<File>) this.console.options.valuesOf("add-plugin");
++ final List<File> list = new ArrayList<>();
++ for (final File file : jars) {
++ if (!file.exists()) {
++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument does not exist, cannot load a plugin from it!", file.getAbsolutePath());
++ continue;
++ }
++ if (!file.isFile()) {
++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument is not a file, cannot load a plugin from it!", file.getAbsolutePath());
++ continue;
++ }
++ if (!file.getName().endsWith(".jar")) {
++ net.minecraft.server.MinecraftServer.LOGGER.warn("File '{}' specified through 'add-plugin' argument is not a jar file, cannot load a plugin from it!", file.getAbsolutePath());
++ continue;
++ }
++ list.add(file);
++ }
++ return list;
++ }
++ // Paper end
++
+ public void enablePlugins(PluginLoadOrder type) {
+ if (type == PluginLoadOrder.STARTUP) {
+ this.helpMap.clear();
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index 1f4dc832ac059ddb9eafd43b0a37436abadaa59f..1c2f232a99856fe754c7805d5b5a3d565e0d7f6f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -160,6 +160,12 @@ public class Main {
+ .ofType(File.class)
+ .defaultsTo(new File("paper.yml"))
+ .describedAs("Yml file");
++
++ acceptsAll(asList("add-plugin", "add-extra-plugin-jar"), "Specify paths to extra plugin jars to be loaded in addition to those in the plugins folder. This argument can be specified multiple times, once for each extra plugin jar path.")
++ .withRequiredArg()
++ .ofType(File.class)
++ .defaultsTo(new File[] {})
++ .describedAs("Jar file");
+ // Paper end
+ }
+ };
diff --git a/patches/server/0026-Support-components-in-ItemMeta.patch b/patches/server/0026-Support-components-in-ItemMeta.patch
new file mode 100644
index 0000000000..cd71224a9b
--- /dev/null
+++ b/patches/server/0026-Support-components-in-ItemMeta.patch
@@ -0,0 +1,83 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: MiniDigger <[email protected]>
+Date: Sat, 6 Jun 2020 18:13:42 +0200
+Subject: [PATCH] Support components in ItemMeta
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+index aa14b5c363824761e81a9a29ae88820841df0166..784db8fa1b9ef99755440c6446248b802445da67 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+@@ -1120,11 +1120,23 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return CraftChatMessage.fromComponent(this.displayName);
+ }
+
++ // Paper start
++ @Override
++ public net.md_5.bungee.api.chat.BaseComponent[] getDisplayNameComponent() {
++ return displayName == null ? new net.md_5.bungee.api.chat.BaseComponent[0] : net.md_5.bungee.chat.ComponentSerializer.parse(CraftChatMessage.toJSON(displayName));
++ }
++ // Paper end
+ @Override
+ public final void setDisplayName(String name) {
+ this.displayName = CraftChatMessage.fromStringOrNull(name);
+ }
+
++ // Paper start
++ @Override
++ public void setDisplayNameComponent(net.md_5.bungee.api.chat.BaseComponent[] component) {
++ this.displayName = CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(component));
++ }
++ // Paper end
+ @Override
+ public boolean hasDisplayName() {
+ return this.displayName != null;
+@@ -1298,6 +1310,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return this.lore == null ? null : new ArrayList<String>(Lists.transform(this.lore, CraftChatMessage::fromComponent));
+ }
+
++ // Paper start
++ @Override
++ public List<net.md_5.bungee.api.chat.BaseComponent[]> getLoreComponents() {
++ return this.lore == null ? null : new ArrayList<>(this.lore.stream().map(entry ->
++ net.md_5.bungee.chat.ComponentSerializer.parse(CraftChatMessage.toJSON(entry))
++ ).collect(java.util.stream.Collectors.toList()));
++ }
++ // Paper end
+ @Override
+ public void setLore(List<String> lore) {
+ if (lore == null || lore.isEmpty()) {
+@@ -1312,6 +1332,21 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ }
+ }
+
++ // Paper start
++ @Override
++ public void setLoreComponents(List<net.md_5.bungee.api.chat.BaseComponent[]> lore) {
++ if (lore == null) {
++ this.lore = null;
++ } else {
++ if (this.lore == null) {
++ safelyAdd(lore, this.lore = new ArrayList<>(lore.size()), false);
++ } else {
++ this.lore.clear();
++ safelyAdd(lore, this.lore, false);
++ }
++ }
++ }
++ // Paper end
+ @Override
+ public boolean hasCustomModelData() {
+ return this.customModelData != null;
+@@ -2209,6 +2244,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ }
+
+ for (Object object : addFrom) {
++ // Paper start - support components
++ if(object instanceof net.md_5.bungee.api.chat.BaseComponent[] baseComponentArr) {
++ addTo.add(CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(baseComponentArr)));
++ } else
++ // Paper end
+ if (!(object instanceof String)) {
+ if (object != null) {
+ // SPIGOT-7399: Null check via if is important,
diff --git a/patches/server/0027-Configurable-cactus-bamboo-and-reed-growth-height.patch b/patches/server/0027-Configurable-cactus-bamboo-and-reed-growth-height.patch
new file mode 100644
index 0000000000..e5e4b807ee
--- /dev/null
+++ b/patches/server/0027-Configurable-cactus-bamboo-and-reed-growth-height.patch
@@ -0,0 +1,92 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 13:02:51 -0600
+Subject: [PATCH] Configurable cactus bamboo and reed growth height
+
+Bamboo - Both the minimum fully-grown height and the maximum are configurable
+- Machine_Maker
+
+diff --git a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java
+index 80bf98e7681cfde3a41ce5676425d5e96089500d..5e88bd02f5c53124f1aeec3eae727a1f83cc8238 100644
+--- a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java
+@@ -137,7 +137,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ if (random.nextFloat() < (world.spigotConfig.bambooModifier / (100.0f * 3)) && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
+ int i = this.getHeightBelowUpToMax(world, pos) + 1;
+
+- if (i < 16) {
++ if (i < world.paperConfig().maxGrowthHeight.bamboo.max) { // Paper - Configurable cactus/bamboo/reed growth height
+ this.growBamboo(state, world, pos, random, i);
+ }
+ }
+@@ -164,7 +164,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ int i = this.getHeightAboveUpToMax(world, pos);
+ int j = this.getHeightBelowUpToMax(world, pos);
+
+- return i + j + 1 < 16 && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1;
++ return i + j + 1 < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height
+ }
+
+ @Override
+@@ -183,7 +183,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ BlockPos blockposition1 = pos.above(i);
+ BlockState iblockdata1 = world.getBlockState(blockposition1);
+
+- if (k >= 16 || !iblockdata1.is(Blocks.BAMBOO) || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here
++ if (k >= world.paperConfig().maxGrowthHeight.bamboo.max || !iblockdata1.is(Blocks.BAMBOO) || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height
+ return;
+ }
+
+@@ -224,7 +224,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ }
+
+ int j = (Integer) state.getValue(BambooStalkBlock.AGE) != 1 && !iblockdata2.is(Blocks.BAMBOO) ? 0 : 1;
+- int k = (height < 11 || random.nextFloat() >= 0.25F) && height != 15 ? 0 : 1;
++ int k = (height < world.paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && height != (world.paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height
+
+ // CraftBukkit start
+ if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, pos.above(), (BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(BambooStalkBlock.AGE, j)).setValue(BambooStalkBlock.LEAVES, blockpropertybamboosize)).setValue(BambooStalkBlock.STAGE, k), 3)) {
+@@ -239,7 +239,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ protected int getHeightAboveUpToMax(BlockGetter world, BlockPos pos) {
+ int i;
+
+- for (i = 0; i < 16 && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) {
++ for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height
+ ;
+ }
+
+@@ -249,7 +249,7 @@ public class BambooStalkBlock extends Block implements BonemealableBlock {
+ protected int getHeightBelowUpToMax(BlockGetter world, BlockPos pos) {
+ int i;
+
+- for (i = 0; i < 16 && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) {
++ for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height
+ ;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java
+index de3df6606979171fd39fa6c5207fd9b0b668ba4e..de1b64e0cbe7f2de63f04262428c9e6ec340916e 100644
+--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java
+@@ -62,7 +62,7 @@ public class CactusBlock extends Block {
+ ;
+ }
+
+- if (i < 3) {
++ if (i < world.paperConfig().maxGrowthHeight.cactus) { // Paper - Configurable cactus/bamboo/reed growth height
+ int j = (Integer) state.getValue(CactusBlock.AGE);
+
+ int modifier = world.spigotConfig.cactusModifier; // Spigot - SPIGOT-7159: Better modifier resolution
+diff --git a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java
+index 161f70de43105c0b49b6c4d0a371dc6036c6813c..547ea09ed84595286c97c128b3b96f6d387ae25f 100644
+--- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java
+@@ -59,7 +59,7 @@ public class SugarCaneBlock extends Block {
+ ;
+ }
+
+- if (i < 3) {
++ if (i < world.paperConfig().maxGrowthHeight.reeds) { // Paper - Configurable cactus/bamboo/reed growth heigh
+ int j = (Integer) state.getValue(SugarCaneBlock.AGE);
+
+ int modifier = world.spigotConfig.caneModifier; // Spigot - SPIGOT-7159: Better modifier resolution
diff --git a/patches/server/0028-Configurable-baby-zombie-movement-speed.patch b/patches/server/0028-Configurable-baby-zombie-movement-speed.patch
new file mode 100644
index 0000000000..a7c6ba5835
--- /dev/null
+++ b/patches/server/0028-Configurable-baby-zombie-movement-speed.patch
@@ -0,0 +1,31 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 13:09:16 -0600
+Subject: [PATCH] Configurable baby zombie movement speed
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
+index 6845a8e13cdc9dd03015ac53b2a62dd706def5cd..3836d9255ac326a7220e1decd2e9d98be7884c17 100644
+--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java
++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
+@@ -77,7 +77,7 @@ import org.bukkit.event.entity.EntityTransformEvent;
+ public class Zombie extends Monster {
+
+ private static final ResourceLocation SPEED_MODIFIER_BABY_ID = ResourceLocation.withDefaultNamespace("baby");
+- private static final AttributeModifier SPEED_MODIFIER_BABY = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, 0.5D, AttributeModifier.Operation.ADD_MULTIPLIED_BASE);
++ private final AttributeModifier babyModifier = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, this.level().paperConfig().entities.behavior.babyZombieMovementModifier, AttributeModifier.Operation.ADD_MULTIPLIED_BASE); // Paper - Make baby speed configurable
+ private static final ResourceLocation REINFORCEMENT_CALLER_CHARGE_ID = ResourceLocation.withDefaultNamespace("reinforcement_caller_charge");
+ private static final AttributeModifier ZOMBIE_REINFORCEMENT_CALLEE_CHARGE = new AttributeModifier(ResourceLocation.withDefaultNamespace("reinforcement_callee_charge"), -0.05000000074505806D, AttributeModifier.Operation.ADD_VALUE);
+ private static final ResourceLocation LEADER_ZOMBIE_BONUS_ID = ResourceLocation.withDefaultNamespace("leader_zombie_bonus");
+@@ -186,9 +186,9 @@ public class Zombie extends Monster {
+ if (this.level() != null && !this.level().isClientSide) {
+ AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
+
+- attributemodifiable.removeModifier(Zombie.SPEED_MODIFIER_BABY_ID);
++ attributemodifiable.removeModifier(this.babyModifier.id()); // Paper - Make baby speed configurable
+ if (baby) {
+- attributemodifiable.addTransientModifier(Zombie.SPEED_MODIFIER_BABY);
++ attributemodifiable.addTransientModifier(this.babyModifier); // Paper - Make baby speed configurable
+ }
+ }
+
diff --git a/patches/server/0029-Configurable-fishing-time-ranges.patch b/patches/server/0029-Configurable-fishing-time-ranges.patch
new file mode 100644
index 0000000000..bbdff28832
--- /dev/null
+++ b/patches/server/0029-Configurable-fishing-time-ranges.patch
@@ -0,0 +1,30 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 13:14:11 -0600
+Subject: [PATCH] Configurable fishing time ranges
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
+index e71f1a9f5347673aa87b20be066046e4d144fd6a..bec8aa80dab4bfc9c75d50e4141f0bf18f4a39cb 100644
+--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
+@@ -93,6 +93,10 @@ public class FishingHook extends Projectile {
+ this.currentState = FishingHook.FishHookState.FLYING;
+ this.luck = Math.max(0, luckBonus);
+ this.lureSpeed = Math.max(0, waitTimeReductionTicks);
++ // Paper start - Configurable fishing time ranges
++ minWaitTime = world.paperConfig().fishingTimeRange.minimum;
++ maxWaitTime = world.paperConfig().fishingTimeRange.maximum;
++ // Paper end - Configurable fishing time ranges
+ }
+
+ public FishingHook(EntityType<? extends FishingHook> type, Level world) {
+@@ -416,7 +420,7 @@ public class FishingHook extends Projectile {
+ } else {
+ // CraftBukkit start - logic to modify fishing wait time
+ this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
+- this.timeUntilLured -= (this.applyLure) ? this.lureSpeed : 0;
++ this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
+ // CraftBukkit end
+ }
+ }
diff --git a/patches/server/0030-Allow-nerfed-mobs-to-jump.patch b/patches/server/0030-Allow-nerfed-mobs-to-jump.patch
new file mode 100644
index 0000000000..3b45d52fe7
--- /dev/null
+++ b/patches/server/0030-Allow-nerfed-mobs-to-jump.patch
@@ -0,0 +1,47 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 13:24:16 -0600
+Subject: [PATCH] Allow nerfed mobs to jump
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
+index 909662e73ce94bb093610ff982fe14fa21754cca..3a63bcdc6a803e7b0b18c863f6e32209ff55707c 100644
+--- a/src/main/java/net/minecraft/world/entity/Mob.java
++++ b/src/main/java/net/minecraft/world/entity/Mob.java
+@@ -123,6 +123,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ private final BodyRotationControl bodyRotationControl;
+ protected PathNavigation navigation;
+ public GoalSelector goalSelector;
++ @Nullable public net.minecraft.world.entity.ai.goal.FloatGoal goalFloat; // Paper - Allow nerfed mobs to jump and float
+ public GoalSelector targetSelector;
+ @Nullable
+ private LivingEntity target;
+@@ -886,7 +887,15 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ @Override
+ protected final void serverAiStep() {
+ ++this.noActionTime;
+- if (!this.aware) return; // CraftBukkit
++ // Paper start - Allow nerfed mobs to jump and float
++ if (!this.aware) {
++ if (goalFloat != null) {
++ if (goalFloat.canUse()) goalFloat.tick();
++ this.getJumpControl().tick();
++ }
++ return;
++ }
++ // Paper end - Allow nerfed mobs to jump and float
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("sensing");
+diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java
+index 5fbb5d2bf8945a361babfe50f5f92fa46b9e8b5f..7eb0e0486203d9ad6ce89d17a4da96a7563088a4 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java
++++ b/src/main/java/net/minecraft/world/entity/ai/goal/FloatGoal.java
+@@ -9,6 +9,7 @@ public class FloatGoal extends Goal {
+
+ public FloatGoal(Mob mob) {
+ this.mob = mob;
++ if (mob.getCommandSenderWorld().paperConfig().entities.behavior.spawnerNerfedMobsShouldJump) mob.goalFloat = this; // Paper - Allow nerfed mobs to jump and float
+ this.setFlags(EnumSet.of(Goal.Flag.JUMP));
+ mob.getNavigation().setCanFloat(true);
+ }
diff --git a/patches/server/0031-Add-configurable-entity-despawn-distances.patch b/patches/server/0031-Add-configurable-entity-despawn-distances.patch
new file mode 100644
index 0000000000..8f3bba25f4
--- /dev/null
+++ b/patches/server/0031-Add-configurable-entity-despawn-distances.patch
@@ -0,0 +1,47 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Suddenly <[email protected]>
+Date: Tue, 1 Mar 2016 13:51:54 -0600
+Subject: [PATCH] Add configurable entity despawn distances
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
+index 3a63bcdc6a803e7b0b18c863f6e32209ff55707c..137e645950eab0fc083ef3ff0fc65ee702dc2ea7 100644
+--- a/src/main/java/net/minecraft/world/entity/Mob.java
++++ b/src/main/java/net/minecraft/world/entity/Mob.java
+@@ -861,20 +861,24 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ Player entityhuman = this.level().getNearestPlayer(this, -1.0D);
+
+ if (entityhuman != null) {
+- double d0 = entityhuman.distanceToSqr((Entity) this);
+- int i = this.getType().getCategory().getDespawnDistance();
+- int j = i * i;
+-
+- if (d0 > (double) j && this.removeWhenFarAway(d0)) {
++ // Paper start - Configurable despawn distances
++ final io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DespawnRangePair despawnRangePair = this.level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory());
++ final io.papermc.paper.configuration.type.DespawnRange.Shape shape = this.level().paperConfig().entities.spawning.despawnRangeShape;
++ final double dy = Math.abs(entityhuman.getY() - this.getY());
++ final double dySqr = Math.pow(dy, 2);
++ final double dxSqr = Math.pow(entityhuman.getX() - this.getX(), 2);
++ final double dzSqr = Math.pow(entityhuman.getZ() - this.getZ(), 2);
++ final double distanceSquared = dxSqr + dzSqr + dySqr;
++ // Despawn if hard/soft limit is exceeded
++ if (despawnRangePair.hard().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy) && this.removeWhenFarAway(distanceSquared)) {
+ this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+ }
+-
+- int k = this.getType().getCategory().getNoDespawnDistance();
+- int l = k * k;
+-
+- if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && d0 > (double) l && this.removeWhenFarAway(d0)) {
+- this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+- } else if (d0 < (double) l) {
++ if (despawnRangePair.soft().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy)) {
++ if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && this.removeWhenFarAway(distanceSquared)) {
++ this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++ }
++ } else {
++ // Paper end - Configurable despawn distances
+ this.noActionTime = 0;
+ }
+ }
diff --git a/patches/server/0032-Drop-falling-block-and-tnt-entities-at-the-specified.patch b/patches/server/0032-Drop-falling-block-and-tnt-entities-at-the-specified.patch
new file mode 100644
index 0000000000..adcac773fb
--- /dev/null
+++ b/patches/server/0032-Drop-falling-block-and-tnt-entities-at-the-specified.patch
@@ -0,0 +1,62 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Byteflux <[email protected]>
+Date: Tue, 1 Mar 2016 14:14:15 -0600
+Subject: [PATCH] Drop falling block and tnt entities at the specified height
+
+Co-authored-by: Jake Potrebic <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
+index d3106ea480e70f5b2ad8d5b21fc353cab660d09c..e8585c05c5db32eafa18f3ac1968ba7e0c6f4566 100644
+--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
++++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
+@@ -159,6 +159,16 @@ public class FallingBlockEntity extends Entity {
+ this.applyGravity();
+ this.move(MoverType.SELF, this.getDeltaMovement());
+ this.applyEffectsFromBlocks();
++ // Paper start - Configurable falling blocks height nerf
++ if (this.level().paperConfig().fixes.fallingBlockHeightNerf.test(v -> this.getY() > v)) {
++ if (this.dropItem && this.level() instanceof final ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
++ this.spawnAtLocation(serverLevel, block);
++ }
++
++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
++ return;
++ }
++ // Paper end - Configurable falling blocks height nerf
+ this.handlePortal();
+ Level world = this.level();
+
+diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
+index 1951fc0af9f441873b7254e8d6c7708f5d697bf9..5bc84cc5ba4dca412dbc159b7a798f52d6f813dc 100644
+--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
++++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
+@@ -106,6 +106,12 @@ public class PrimedTnt extends Entity implements TraceableEntity {
+ this.applyGravity();
+ this.move(MoverType.SELF, this.getDeltaMovement());
+ this.applyEffectsFromBlocks();
++ // Paper start - Configurable TNT height nerf
++ if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) {
++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
++ return;
++ }
++ // Paper end - Configurable TNT height nerf
+ this.setDeltaMovement(this.getDeltaMovement().scale(0.98D));
+ if (this.onGround()) {
+ this.setDeltaMovement(this.getDeltaMovement().multiply(0.7D, -0.5D, 0.7D));
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java
+index f6bc771e3e7e62c80326a787b568a0a7c5627813..0f005b1f98c387cd7bcfb934f44c166c39fdc9a5 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartTNT.java
+@@ -56,6 +56,12 @@ public class MinecartTNT extends AbstractMinecart {
+ public void tick() {
+ super.tick();
+ if (this.fuse > 0) {
++ // Paper start - Configurable TNT height nerf
++ if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) {
++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
++ return;
++ }
++ // Paper end - Configurable TNT height nerf
+ --this.fuse;
+ this.level().addParticle(ParticleTypes.SMOKE, this.getX(), this.getY() + 0.5D, this.getZ(), 0.0D, 0.0D, 0.0D);
+ } else if (this.fuse == 0) {
diff --git a/patches/server/0033-Expose-server-build-information.patch b/patches/server/0033-Expose-server-build-information.patch
new file mode 100644
index 0000000000..e402cda0da
--- /dev/null
+++ b/patches/server/0033-Expose-server-build-information.patch
@@ -0,0 +1,758 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 14:32:43 -0600
+Subject: [PATCH] Expose server build information
+
+Co-authored-by: Zach Brown <[email protected]>
+Co-authored-by: Kyle Wood <[email protected]>
+Co-authored-by: Mark Vainomaa <[email protected]>
+Co-authored-by: Riley Park <[email protected]>
+Co-authored-by: Jake Potrebic <[email protected]>
+Co-authored-by: masmc05 <[email protected]>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 49749e2bd8a4af96d2091fa1bccd876c2abb9e12..221c7dace9950bd4e57299eeff46b2ee4cd05258 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -1,4 +1,5 @@
+ import io.papermc.paperweight.util.*
++import java.time.Instant
+
+ plugins {
+ java
+@@ -79,18 +80,24 @@ tasks.jar {
+
+ manifest {
+ val git = Git(rootProject.layout.projectDirectory.path)
++ val mcVersion = rootProject.providers.gradleProperty("mcVersion").get()
++ val build = System.getenv("BUILD_NUMBER") ?: null
+ val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim()
+- val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\""
++ val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash"
+ val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper
+ val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper
+ attributes(
+ "Main-Class" to "org.bukkit.craftbukkit.Main",
+- "Implementation-Title" to "CraftBukkit",
+- "Implementation-Version" to "git-Paper-$implementationVersion",
++ "Implementation-Title" to "Paper",
++ "Implementation-Version" to implementationVersion,
+ "Implementation-Vendor" to date, // Paper
+- "Specification-Title" to "Bukkit",
++ "Specification-Title" to "Paper",
+ "Specification-Version" to project.version,
+- "Specification-Vendor" to "Bukkit Team",
++ "Specification-Vendor" to "Paper Team",
++ "Brand-Id" to "papermc:paper",
++ "Brand-Name" to "Paper",
++ "Build-Number" to (build ?: ""),
++ "Build-Time" to Instant.now().toString(),
+ "Git-Branch" to gitBranch, // Paper
+ "Git-Commit" to gitHash, // Paper
+ "CraftBukkit-Package-Version" to paperweight.craftBukkitPackageVersion.get(), // Paper
+diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..532306cacd52579cdf37e4aca25887b1ed3ba6a1
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+@@ -0,0 +1,146 @@
++package com.destroystokyo.paper;
++
++import com.destroystokyo.paper.util.VersionFetcher;
++import com.google.common.base.Charsets;
++import com.google.common.io.Resources;
++import com.google.gson.Gson;
++import com.google.gson.JsonArray;
++import com.google.gson.JsonElement;
++import com.google.gson.JsonObject;
++import com.google.gson.JsonSyntaxException;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.ServerBuildInfo;
++import java.io.BufferedReader;
++import java.io.IOException;
++import java.io.InputStreamReader;
++import java.net.HttpURLConnection;
++import java.net.URI;
++import java.util.Optional;
++import java.util.OptionalInt;
++import java.util.stream.StreamSupport;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.TextDecoration;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.slf4j.Logger;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.TextColor.color;
++
++@DefaultQualifier(NonNull.class)
++public class PaperVersionFetcher implements VersionFetcher {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final int DISTANCE_ERROR = -1;
++ private static final int DISTANCE_UNKNOWN = -2;
++ private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper";
++
++ @Override
++ public long getCacheTime() {
++ return 720000;
++ }
++
++ @Override
++ public Component getVersionMessage(final String serverVersion) {
++ final Component updateMessage;
++ final ServerBuildInfo build = ServerBuildInfo.buildInfo();
++ if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) {
++ updateMessage = text("You are running a development version without access to version information", color(0xFF5300));
++ } else {
++ updateMessage = getUpdateStatusMessage("PaperMC/Paper", build);
++ }
++ final @Nullable Component history = this.getHistory();
++
++ return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage;
++ }
++
++ private static Component getUpdateStatusMessage(final String repo, final ServerBuildInfo build) {
++ int distance = DISTANCE_ERROR;
++
++ final OptionalInt buildNumber = build.buildNumber();
++ if (buildNumber.isPresent()) {
++ distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt());
++ } else {
++ final Optional<String> gitBranch = build.gitBranch();
++ final Optional<String> gitCommit = build.gitCommit();
++ if (gitBranch.isPresent() && gitCommit.isPresent()) {
++ distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get());
++ }
++ }
++
++ return switch (distance) {
++ case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW);
++ case 0 -> text("You are running the latest version", NamedTextColor.GREEN);
++ case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW);
++ default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW)
++ .append(Component.newline())
++ .append(text("Download the new version at: ")
++ .append(text(DOWNLOAD_PAGE, NamedTextColor.GOLD)
++ .hoverEvent(text("Click to open", NamedTextColor.WHITE))
++ .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE))));
++ };
++ }
++
++ private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) {
++ try {
++ try (final BufferedReader reader = Resources.asCharSource(
++ URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(),
++ Charsets.UTF_8
++ ).openBufferedStream()) {
++ final JsonObject json = new Gson().fromJson(reader, JsonObject.class);
++ final JsonArray builds = json.getAsJsonArray("builds");
++ final int latest = StreamSupport.stream(builds.spliterator(), false)
++ .mapToInt(JsonElement::getAsInt)
++ .max()
++ .orElseThrow();
++ return latest - jenkinsBuild;
++ } catch (final JsonSyntaxException ex) {
++ LOGGER.error("Error parsing json from Paper's downloads API", ex);
++ return DISTANCE_ERROR;
++ }
++ } catch (final IOException e) {
++ LOGGER.error("Error while parsing version", e);
++ return DISTANCE_ERROR;
++ }
++ }
++
++ // Contributed by Techcable <[email protected]> in GH-65
++ private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) {
++ try {
++ final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection();
++ connection.connect();
++ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return DISTANCE_UNKNOWN; // Unknown commit
++ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
++ final JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
++ final String status = obj.get("status").getAsString();
++ return switch (status) {
++ case "identical" -> 0;
++ case "behind" -> obj.get("behind_by").getAsInt();
++ default -> DISTANCE_ERROR;
++ };
++ } catch (final JsonSyntaxException | NumberFormatException e) {
++ LOGGER.error("Error parsing json from GitHub's API", e);
++ return DISTANCE_ERROR;
++ }
++ } catch (final IOException e) {
++ LOGGER.error("Error while parsing version", e);
++ return DISTANCE_ERROR;
++ }
++ }
++
++ private @Nullable Component getHistory() {
++ final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData();
++ if (data == null) {
++ return null;
++ }
++
++ final @Nullable String oldVersion = data.getOldVersion();
++ if (oldVersion == null) {
++ return null;
++ }
++
++ return text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..660b2ec6b63a4ceffee44ab11f54dfa7c0d0996f
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/VersionHistoryManager.java
+@@ -0,0 +1,153 @@
++package com.destroystokyo.paper;
++
++import com.google.common.base.MoreObjects;
++import com.google.gson.Gson;
++import com.google.gson.JsonSyntaxException;
++import java.io.BufferedReader;
++import java.io.BufferedWriter;
++import java.io.IOException;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.Paths;
++import java.nio.file.StandardOpenOption;
++import java.util.Objects;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++import org.bukkit.Bukkit;
++
++import javax.annotation.Nonnull;
++import javax.annotation.Nullable;
++
++public enum VersionHistoryManager {
++ INSTANCE;
++
++ private final Gson gson = new Gson();
++
++ private final Logger logger = Bukkit.getLogger();
++
++ private VersionData currentData = null;
++
++ VersionHistoryManager() {
++ final Path path = Paths.get("version_history.json");
++
++ if (Files.exists(path)) {
++ // Basic file santiy checks
++ if (!Files.isRegularFile(path)) {
++ if (Files.isDirectory(path)) {
++ logger.severe(path + " is a directory, cannot be used for version history");
++ } else {
++ logger.severe(path + " is not a regular file, cannot be used for version history");
++ }
++ // We can't continue
++ return;
++ }
++
++ try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
++ currentData = gson.fromJson(reader, VersionData.class);
++ } catch (final IOException e) {
++ logger.log(Level.SEVERE, "Failed to read version history file '" + path + "'", e);
++ return;
++ } catch (final JsonSyntaxException e) {
++ logger.log(Level.SEVERE, "Invalid json syntax for file '" + path + "'", e);
++ return;
++ }
++
++ final String version = Bukkit.getVersion();
++ if (version == null) {
++ logger.severe("Failed to retrieve current version");
++ return;
++ }
++
++ if (currentData == null) {
++ // Empty file
++ currentData = new VersionData();
++ currentData.setCurrentVersion(version);
++ writeFile(path);
++ return;
++ }
++
++ if (!version.equals(currentData.getCurrentVersion())) {
++ // The version appears to have changed
++ currentData.setOldVersion(currentData.getCurrentVersion());
++ currentData.setCurrentVersion(version);
++ writeFile(path);
++ }
++ } else {
++ // File doesn't exist, start fresh
++ currentData = new VersionData();
++ // oldVersion is null
++ currentData.setCurrentVersion(Bukkit.getVersion());
++ writeFile(path);
++ }
++ }
++
++ private void writeFile(@Nonnull final Path path) {
++ try (final BufferedWriter writer = Files.newBufferedWriter(
++ path,
++ StandardCharsets.UTF_8,
++ StandardOpenOption.WRITE,
++ StandardOpenOption.CREATE,
++ StandardOpenOption.TRUNCATE_EXISTING
++ )) {
++ gson.toJson(currentData, writer);
++ } catch (final IOException e) {
++ logger.log(Level.SEVERE, "Failed to write to version history file", e);
++ }
++ }
++
++ @Nullable
++ public VersionData getVersionData() {
++ return currentData;
++ }
++
++ public static class VersionData {
++ private String oldVersion;
++
++ private String currentVersion;
++
++ @Nullable
++ public String getOldVersion() {
++ return oldVersion;
++ }
++
++ public void setOldVersion(@Nullable String oldVersion) {
++ this.oldVersion = oldVersion;
++ }
++
++ @Nullable
++ public String getCurrentVersion() {
++ return currentVersion;
++ }
++
++ public void setCurrentVersion(@Nullable String currentVersion) {
++ this.currentVersion = currentVersion;
++ }
++
++ @Override
++ public String toString() {
++ return MoreObjects.toStringHelper(this)
++ .add("oldVersion", oldVersion)
++ .add("currentVersion", currentVersion)
++ .toString();
++ }
++
++ @Override
++ public boolean equals(@Nullable Object o) {
++ if (this == o) {
++ return true;
++ }
++ if (o == null || getClass() != o.getClass()) {
++ return false;
++ }
++ final VersionData versionData = (VersionData) o;
++ return Objects.equals(oldVersion, versionData.oldVersion) &&
++ Objects.equals(currentVersion, versionData.currentVersion);
++ }
++
++ @Override
++ public int hashCode() {
++ return Objects.hash(oldVersion, currentVersion);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d543b1b107ab8d3eeb1fc3c1cadf489928d2786e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/PaperBootstrap.java
+@@ -0,0 +1,55 @@
++package io.papermc.paper;
++
++import java.util.List;
++import joptsimple.OptionSet;
++import net.minecraft.SharedConstants;
++import net.minecraft.server.Main;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++
++public final class PaperBootstrap {
++ private static final Logger LOGGER = LoggerFactory.getLogger("bootstrap");
++
++ private PaperBootstrap() {
++ }
++
++ public static void boot(final OptionSet options) {
++ SharedConstants.tryDetectVersion();
++
++ getStartupVersionMessages().forEach(LOGGER::info);
++
++ Main.main(options);
++ }
++
++ private static List<String> getStartupVersionMessages() {
++ final String javaSpecVersion = System.getProperty("java.specification.version");
++ final String javaVmName = System.getProperty("java.vm.name");
++ final String javaVmVersion = System.getProperty("java.vm.version");
++ final String javaVendor = System.getProperty("java.vendor");
++ final String javaVendorVersion = System.getProperty("java.vendor.version");
++ final String osName = System.getProperty("os.name");
++ final String osVersion = System.getProperty("os.version");
++ final String osArch = System.getProperty("os.arch");
++
++ final ServerBuildInfo bi = ServerBuildInfo.buildInfo();
++ return List.of(
++ String.format(
++ "Running Java %s (%s %s; %s %s) on %s %s (%s)",
++ javaSpecVersion,
++ javaVmName,
++ javaVmVersion,
++ javaVendor,
++ javaVendorVersion,
++ osName,
++ osVersion,
++ osArch
++ ),
++ String.format(
++ "Loading %s %s for Minecraft %s",
++ bi.brandName(),
++ bi.asString(ServerBuildInfo.StringRepresentation.VERSION_FULL),
++ bi.minecraftVersionId()
++ )
++ );
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..790bad0494454ca12ee152e3de6da3da634d9b20
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java
+@@ -0,0 +1,104 @@
++package io.papermc.paper;
++
++import com.google.common.base.Strings;
++import io.papermc.paper.util.JarManifests;
++import java.time.Instant;
++import java.time.temporal.ChronoUnit;
++import java.util.Optional;
++import java.util.OptionalInt;
++import java.util.jar.Manifest;
++import net.kyori.adventure.key.Key;
++import net.minecraft.SharedConstants;
++import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.craftbukkit.Main;
++import org.jetbrains.annotations.NotNull;
++
++public record ServerBuildInfoImpl(
++ Key brandId,
++ String brandName,
++ String minecraftVersionId,
++ String minecraftVersionName,
++ OptionalInt buildNumber,
++ Instant buildTime,
++ Optional<String> gitBranch,
++ Optional<String> gitCommit
++) implements ServerBuildInfo {
++ private static final String ATTRIBUTE_BRAND_ID = "Brand-Id";
++ private static final String ATTRIBUTE_BRAND_NAME = "Brand-Name";
++ private static final String ATTRIBUTE_BUILD_TIME = "Build-Time";
++ private static final String ATTRIBUTE_BUILD_NUMBER = "Build-Number";
++ private static final String ATTRIBUTE_GIT_BRANCH = "Git-Branch";
++ private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit";
++
++ private static final String BRAND_PAPER_NAME = "Paper";
++
++ private static final String BUILD_DEV = "DEV";
++
++ public ServerBuildInfoImpl() {
++ this(JarManifests.manifest(CraftServer.class));
++ }
++
++ private ServerBuildInfoImpl(final Manifest manifest) {
++ this(
++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID)
++ .map(Key::key)
++ .orElse(BRAND_PAPER_ID),
++ getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME)
++ .orElse(BRAND_PAPER_NAME),
++ SharedConstants.getCurrentVersion().getId(),
++ SharedConstants.getCurrentVersion().getName(),
++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER)
++ .map(Integer::parseInt)
++ .map(OptionalInt::of)
++ .orElse(OptionalInt.empty()),
++ getManifestAttribute(manifest, ATTRIBUTE_BUILD_TIME)
++ .map(Instant::parse)
++ .orElse(Main.BOOT_TIME),
++ getManifestAttribute(manifest, ATTRIBUTE_GIT_BRANCH),
++ getManifestAttribute(manifest, ATTRIBUTE_GIT_COMMIT)
++ );
++ }
++
++ @Override
++ public boolean isBrandCompatible(final @NotNull Key brandId) {
++ return brandId.equals(this.brandId);
++ }
++
++ @Override
++ public @NotNull String asString(final @NotNull StringRepresentation representation) {
++ final StringBuilder sb = new StringBuilder();
++ sb.append(this.minecraftVersionId);
++ sb.append('-');
++ if (this.buildNumber.isPresent()) {
++ sb.append(this.buildNumber.getAsInt());
++ } else {
++ sb.append(BUILD_DEV);
++ }
++ final boolean hasGitBranch = this.gitBranch.isPresent();
++ final boolean hasGitCommit = this.gitCommit.isPresent();
++ if (hasGitBranch || hasGitCommit) {
++ sb.append('-');
++ }
++ if (hasGitBranch && representation == StringRepresentation.VERSION_FULL) {
++ sb.append(this.gitBranch.get());
++ if (hasGitCommit) {
++ sb.append('@');
++ }
++ }
++ if (hasGitCommit) {
++ sb.append(this.gitCommit.get());
++ }
++ if (representation == StringRepresentation.VERSION_FULL) {
++ sb.append(' ');
++ sb.append('(');
++ sb.append(this.buildTime.truncatedTo(ChronoUnit.SECONDS));
++ sb.append(')');
++ }
++ return sb.toString();
++ }
++
++ private static Optional<String> getManifestAttribute(final Manifest manifest, final String name) {
++ final String value = manifest != null ? manifest.getMainAttributes().getValue(name) : null;
++ return Optional.ofNullable(Strings.emptyToNull(value));
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 6ae26c5e2a60ee04c786cf19b08678c50c397a6a..f70468b2cc4a782d3d7cc7854192fbbc3ad3020f 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -45,7 +45,6 @@ import java.util.Set;
+ import java.util.UUID;
+ import java.util.concurrent.CompletableFuture;
+ import java.util.concurrent.Executor;
+-import java.util.concurrent.RejectedExecutionException;
+ import java.util.concurrent.atomic.AtomicReference;
+ import java.util.concurrent.locks.LockSupport;
+ import java.util.function.BooleanSupplier;
+@@ -198,8 +197,6 @@ import net.minecraft.world.phys.Vec2;
+ import net.minecraft.world.phys.Vec3;
+ import org.bukkit.Bukkit;
+ import org.bukkit.craftbukkit.CraftRegistry;
+-import org.bukkit.craftbukkit.CraftServer;
+-import org.bukkit.craftbukkit.Main;
+ import org.bukkit.event.server.ServerLoadEvent;
+ // CraftBukkit end
+
+@@ -1740,7 +1737,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ @DontObfuscate
+ public String getServerModName() {
+- return "Spigot"; // Spigot - Spigot > // CraftBukkit - cb > vanilla!
++ return io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
+ }
+
+ public SystemReport fillSystemReport(SystemReport details) {
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index a129ddfe7b00d6abab94437806a5cfb9668e7cc9..e2cb85c8f121837e8a19e003e1e757f431dfaf2b 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -215,6 +215,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ // Paper end - initialize global and world-defaults configuration
+ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
+ com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
++ com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now
+
+ this.setPvpAllowed(dedicatedserverproperties.pvp);
+ this.setFlightAllowed(dedicatedserverproperties.allowFlight);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
+index f077b8ff0bf0d96628db3569132696b68fd79921..5f11f5b16766f9d1d5640ae037e259bed9020384 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java
+@@ -18,8 +18,10 @@ public class CraftCrashReport implements Supplier<String> {
+
+ @Override
+ public String get() {
++ final io.papermc.paper.ServerBuildInfo build = io.papermc.paper.ServerBuildInfo.buildInfo(); // Paper
+ StringWriter value = new StringWriter();
+ try {
++ value.append("\n BrandInfo: ").append(String.format("%s (%s) version %s", build.brandName(), build.brandId(), build.asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_FULL))); // Paper
+ value.append("\n Running: ").append(Bukkit.getName()).append(" version ").append(Bukkit.getVersion()).append(" (Implementing API version ").append(Bukkit.getBukkitVersion()).append(") ").append(String.valueOf(MinecraftServer.getServer().usesAuthentication()));
+ value.append("\n Plugins: {");
+ for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 16d2b3e59b8a6ef65b411afb9d94c61e6d797e36..e4335bfc98272c5499651977625e1f0ca671fbec 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -11,8 +11,6 @@ import com.google.common.collect.MapMaker;
+ import com.mojang.authlib.GameProfile;
+ import com.mojang.brigadier.StringReader;
+ import com.mojang.brigadier.exceptions.CommandSyntaxException;
+-import com.mojang.brigadier.tree.CommandNode;
+-import com.mojang.brigadier.tree.LiteralCommandNode;
+ import com.mojang.serialization.Dynamic;
+ import com.mojang.serialization.Lifecycle;
+ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+@@ -27,7 +25,6 @@ import java.net.InetAddress;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.Date;
+-import java.util.HashMap;
+ import java.util.HashSet;
+ import java.util.Iterator;
+ import java.util.LinkedHashMap;
+@@ -155,7 +152,6 @@ import org.bukkit.craftbukkit.ban.CraftProfileBanList;
+ import org.bukkit.craftbukkit.block.data.CraftBlockData;
+ import org.bukkit.craftbukkit.boss.CraftBossBar;
+ import org.bukkit.craftbukkit.boss.CraftKeyedBossbar;
+-import org.bukkit.craftbukkit.command.BukkitCommandWrapper;
+ import org.bukkit.craftbukkit.command.CraftCommandMap;
+ import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
+ import org.bukkit.craftbukkit.entity.CraftEntityFactory;
+@@ -254,7 +250,6 @@ import org.bukkit.plugin.PluginManager;
+ import org.bukkit.plugin.ServicesManager;
+ import org.bukkit.plugin.SimplePluginManager;
+ import org.bukkit.plugin.SimpleServicesManager;
+-import org.bukkit.plugin.java.JavaPluginLoader;
+ import org.bukkit.plugin.messaging.Messenger;
+ import org.bukkit.plugin.messaging.StandardMessenger;
+ import org.bukkit.profile.PlayerProfile;
+@@ -271,7 +266,7 @@ import org.yaml.snakeyaml.error.MarkedYAMLException;
+ import net.md_5.bungee.api.chat.BaseComponent; // Spigot
+
+ public final class CraftServer implements Server {
+- private final String serverName = "CraftBukkit";
++ private final String serverName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
+ private final String serverVersion;
+ private final String bukkitVersion = Versioning.getBukkitVersion();
+ private final Logger logger = Logger.getLogger("Minecraft");
+@@ -327,7 +322,7 @@ public final class CraftServer implements Server {
+ return player.getBukkitEntity();
+ }
+ }));
+- this.serverVersion = CraftServer.class.getPackage().getImplementationVersion();
++ this.serverVersion = io.papermc.paper.ServerBuildInfo.buildInfo().asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); // Paper - improve version
+ this.structureManager = new CraftStructureManager(console.getStructureManager(), console.registryAccess());
+ this.dataPackManager = new CraftDataPackManager(this.getServer().getPackRepository());
+ this.serverTickManager = new CraftServerTickManager(console.tickRateManager());
+@@ -609,6 +604,13 @@ public final class CraftServer implements Server {
+ return this.bukkitVersion;
+ }
+
++ // Paper start - expose game version
++ @Override
++ public String getMinecraftVersion() {
++ return console.getServerVersion();
++ }
++ // Paper end
++
+ @Override
+ public List<CraftPlayer> getOnlinePlayers() {
+ return this.playerView;
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index 1c2f232a99856fe754c7805d5b5a3d565e0d7f6f..c8171cb14612857a5a6f7b000c1cdfb62a59836f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -15,6 +15,7 @@ import joptsimple.OptionSet;
+ import joptsimple.util.PathConverter;
+
+ public class Main {
++ public static final java.time.Instant BOOT_TIME = java.time.Instant.now(); // Paper - track initial start time
+ public static boolean useJline = true;
+ public static boolean useConsole = true;
+
+@@ -241,7 +242,7 @@ public class Main {
+ deadline.add(Calendar.DAY_OF_YEAR, -2);
+ if (buildDate.before(deadline.getTime())) {
+ System.err.println("*** Error, this build is outdated ***");
+- System.err.println("*** Please download a new build as per instructions from https://www.spigotmc.org/go/outdated-spigot ***");
++ System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads/paper ***"); // Paper
+ System.err.println("*** Server will start in 20 seconds ***");
+ Thread.sleep(TimeUnit.SECONDS.toMillis(20));
+ }
+@@ -249,8 +250,9 @@ public class Main {
+
+ System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
+ System.setProperty("jdk.console", "java.base"); // Paper - revert default console provider back to java.base so we can have our own jline
+- System.out.println("Loading libraries, please wait...");
+- net.minecraft.server.Main.main(options);
++ //System.out.println("Loading libraries, please wait...");
++ //net.minecraft.server.Main.main(options);
++ io.papermc.paper.PaperBootstrap.boot(options);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 07316f0043639c608dddadd1b3af871f4c3129d0..e27f10d0d5720c144729ce83e27aa1c70170ebe2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -475,6 +475,13 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ return this.customBiome;
+ }
+
++ // Paper start
++ @Override
++ public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
++ return new com.destroystokyo.paper.PaperVersionFetcher();
++ }
++ // Paper end
++
+ /**
+ * This helper class represents the different NBT Tags.
+ * <p>
+diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
+index f697d45e0ac4e9cdc8a46121510a04c0f294d91f..e086765dec32241bc5a77afcf072c77a40c6d785 100644
+--- a/src/main/java/org/spigotmc/WatchdogThread.java
++++ b/src/main/java/org/spigotmc/WatchdogThread.java
+@@ -19,7 +19,7 @@ public class WatchdogThread extends Thread
+
+ private WatchdogThread(long timeoutTime, boolean restart)
+ {
+- super( "Spigot Watchdog Thread" );
++ super( "Paper Watchdog Thread" );
+ this.timeoutTime = timeoutTime;
+ this.restart = restart;
+ }
+@@ -65,14 +65,14 @@ public class WatchdogThread extends Thread
+ {
+ Logger log = Bukkit.getServer().getLogger();
+ log.log( Level.SEVERE, "------------------------------" );
+- log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Spigot bug." );
++ log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper
+ log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" );
+ log.log( Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring" );
+ log.log( Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once" );
+ log.log( Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes" );
+- log.log( Level.SEVERE, "If you are unsure or still think this is a Spigot bug, please report to https://www.spigotmc.org/" );
++ log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" );
+ log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" );
+- log.log( Level.SEVERE, "Spigot version: " + Bukkit.getServer().getVersion() );
++ log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() );
+ //
+ if ( net.minecraft.world.level.Level.lastPhysicsProblem != null )
+ {
+@@ -82,7 +82,7 @@ public class WatchdogThread extends Thread
+ }
+ //
+ log.log( Level.SEVERE, "------------------------------" );
+- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" );
++ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
+ log.log( Level.SEVERE, "------------------------------" );
+ //
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
+new file mode 100644
+index 0000000000000000000000000000000000000000..79b4b25784cfeabd5f619ed5454ef843f35041db
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo
+@@ -0,0 +1 @@
++io.papermc.paper.ServerBuildInfoImpl
diff --git a/patches/server/0034-Player-affects-spawning-API.patch b/patches/server/0034-Player-affects-spawning-API.patch
new file mode 100644
index 0000000000..09c8974a75
--- /dev/null
+++ b/patches/server/0034-Player-affects-spawning-API.patch
@@ -0,0 +1,158 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jedediah Smith <[email protected]>
+Date: Tue, 1 Mar 2016 14:47:52 -0600
+Subject: [PATCH] Player affects spawning API
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java
+index 048c8af16fad8708a486bb29304db22e2fb1ecb3..a617ea34cfc28cefd68dd14ffbb334b87f98f65c 100644
+--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java
++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java
+@@ -29,6 +29,11 @@ public final class EntitySelector {
+ public static final Predicate<Entity> CAN_BE_PICKED = EntitySelector.NO_SPECTATORS.and(Entity::isPickable);
+
+ private EntitySelector() {}
++ // Paper start - Affects Spawning API
++ public static final Predicate<Entity> PLAYER_AFFECTS_SPAWNING = (entity) -> {
++ return !entity.isSpectator() && entity.isAlive() && entity instanceof Player player && player.affectsSpawning;
++ };
++ // Paper end - Affects Spawning API
+
+ public static Predicate<Entity> withinDistance(double x, double y, double z, double max) {
+ double d4 = max * max;
+diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
+index 137e645950eab0fc083ef3ff0fc65ee702dc2ea7..0562039114a90ddb64547eb8396920813d54f46b 100644
+--- a/src/main/java/net/minecraft/world/entity/Mob.java
++++ b/src/main/java/net/minecraft/world/entity/Mob.java
+@@ -858,7 +858,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
+ this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+ } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
+- Player entityhuman = this.level().getNearestPlayer(this, -1.0D);
++ Player entityhuman = this.level().findNearbyPlayer(this, -1.0D, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper - Affects Spawning API
+
+ if (entityhuman != null) {
+ // Paper start - Configurable despawn distances
+diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
+index c86e2d75ac627e9c92a6e006cb4c06ec6a2cb91e..521b09ac14372f524b06ffdce57932d0a590700b 100644
+--- a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
++++ b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
+@@ -27,7 +27,7 @@ public class SkeletonTrapGoal extends Goal {
+
+ @Override
+ public boolean canUse() {
+- return this.horse.level().hasNearbyAlivePlayer(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D);
++ return this.horse.level().hasNearbyAlivePlayerThatAffectsSpawning(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D); // Paper - Affects Spawning API
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
+index 0594b6adcb849bba2c810de245a3bdaeeca0be60..52d8ea3e40cdb01eab428f5d3d945c0c9f6088ce 100644
+--- a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
++++ b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
+@@ -123,7 +123,7 @@ public class Silverfish extends Monster {
+ } else {
+ Player entityhuman = world.getNearestPlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, 5.0D, true);
+
+- return entityhuman == null;
++ return !(entityhuman != null && !entityhuman.affectsSpawning) && entityhuman == null; // Paper - Affects Spawning API
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
+index 3836d9255ac326a7220e1decd2e9d98be7884c17..73c4585870b7af409f84474f126a58497ed0495f 100644
+--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java
++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
+@@ -349,7 +349,7 @@ public class Zombie extends Monster {
+
+ if (SpawnPlacements.isSpawnPositionOk(entitytypes, world, blockposition) && SpawnPlacements.checkSpawnRules(entitytypes, world, EntitySpawnReason.REINFORCEMENT, blockposition, world.random)) {
+ entityzombie.setPos((double) i1, (double) j1, (double) k1);
+- if (!world.hasNearbyAlivePlayer((double) i1, (double) j1, (double) k1, 7.0D) && world.isUnobstructed(entityzombie) && world.noCollision((Entity) entityzombie) && (entityzombie.canSpawnInLiquids() || !world.containsAnyLiquid(entityzombie.getBoundingBox()))) {
++ if (!world.hasNearbyAlivePlayerThatAffectsSpawning((double) i1, (double) j1, (double) k1, 7.0D) && world.isUnobstructed(entityzombie) && world.noCollision((Entity) entityzombie) && (entityzombie.canSpawnInLiquids() || !world.containsAnyLiquid(entityzombie.getBoundingBox()))) { // Paper - affects spawning api
+ entityzombie.setTarget(entityliving, EntityTargetEvent.TargetReason.REINFORCEMENT_TARGET, true); // CraftBukkit
+ entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), EntitySpawnReason.REINFORCEMENT, (SpawnGroupData) null);
+ world.addFreshEntityWithPassengers(entityzombie, CreatureSpawnEvent.SpawnReason.REINFORCEMENTS); // CraftBukkit
+diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
+index f917b43240b2fa4207b276ac850832fd1618302a..09a7c6171e50b6cf08cf7096b6005d3f9d8d2de7 100644
+--- a/src/main/java/net/minecraft/world/entity/player/Player.java
++++ b/src/main/java/net/minecraft/world/entity/player/Player.java
+@@ -198,6 +198,7 @@ public abstract class Player extends LivingEntity {
+ public Entity currentExplosionCause;
+ private boolean ignoreFallDamageFromCurrentImpulse;
+ private int currentImpulseContextResetGraceTime;
++ public boolean affectsSpawning = true; // Paper - Affects Spawning API
+
+ // CraftBukkit start
+ public boolean fauxSleeping;
+diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java
+index b2bed46f809abee056aa99f39f26f8c0fbf0036c..366661561544f8e99f238583259991e9fcbab8af 100644
+--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java
++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java
+@@ -58,7 +58,7 @@ public abstract class BaseSpawner {
+ }
+
+ public boolean isNearPlayer(Level world, BlockPos pos) {
+- return world.hasNearbyAlivePlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange);
++ return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API
+ }
+
+ public void clientTick(Level world, BlockPos pos) {
+diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
+index f689b2ca0ebc15c099f36ebfd14e455bda540296..fb043d67eaa6336fc0b5d62774b8f1107f9dfa1e 100644
+--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
+@@ -71,6 +71,11 @@ public interface EntityGetter {
+ }
+ }
+
++ // Paper start - Affects Spawning API
++ default @Nullable Player findNearbyPlayer(Entity entity, double maxDistance, @Nullable Predicate<Entity> predicate) {
++ return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), maxDistance, predicate);
++ }
++ // Paper end - Affects Spawning API
+ @Nullable
+ default Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate<Entity> targetPredicate) {
+ double d = -1.0;
+@@ -100,6 +105,20 @@ public interface EntityGetter {
+ return this.getNearestPlayer(x, y, z, maxDistance, predicate);
+ }
+
++ // Paper start - Affects Spawning API
++ default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) {
++ for (Player player : this.players()) {
++ if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check
++ double distanceSqr = player.distanceToSqr(x, y, z);
++ if (range < 0.0D || distanceSqr < range * range) {
++ return true;
++ }
++ }
++ }
++ return false;
++ }
++ // Paper end - Affects Spawning API
++
+ default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) {
+ for (Player player : this.players()) {
+ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index a37e5d822e93b3a4b1c0becf22290017c4709b94..90428fbc332fb8b621725ade8eb010d5edec1286 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2437,6 +2437,17 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ return this.getHandle().language;
+ }
+
++ // Paper start
++ public void setAffectsSpawning(boolean affects) {
++ this.getHandle().affectsSpawning = affects;
++ }
++
++ @Override
++ public boolean getAffectsSpawning() {
++ return this.getHandle().affectsSpawning;
++ }
++ // Paper end
++
+ @Override
+ public void updateCommands() {
+ if (this.getHandle().connection == null) return;
diff --git a/patches/server/0035-Only-refresh-abilities-if-needed.patch b/patches/server/0035-Only-refresh-abilities-if-needed.patch
new file mode 100644
index 0000000000..372aa1ca7b
--- /dev/null
+++ b/patches/server/0035-Only-refresh-abilities-if-needed.patch
@@ -0,0 +1,25 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 23:12:03 -0600
+Subject: [PATCH] Only refresh abilities if needed
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 90428fbc332fb8b621725ade8eb010d5edec1286..fcaac0479ef0259d2271de0cd12752833873a1f6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2120,12 +2120,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public void setFlying(boolean value) {
++ boolean needsUpdate = getHandle().getAbilities().flying != value; // Paper - Only refresh abilities if needed
+ if (!this.getAllowFlight()) {
+ Preconditions.checkArgument(!value, "Player is not allowed to fly (check #getAllowFlight())");
+ }
+
+ this.getHandle().getAbilities().flying = value;
+- this.getHandle().onUpdateAbilities();
++ if (needsUpdate) this.getHandle().onUpdateAbilities(); // Paper - Only refresh abilities if needed
+ }
+
+ @Override
diff --git a/patches/server/0036-Entity-Origin-API.patch b/patches/server/0036-Entity-Origin-API.patch
new file mode 100644
index 0000000000..17636f53d1
--- /dev/null
+++ b/patches/server/0036-Entity-Origin-API.patch
@@ -0,0 +1,121 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Byteflux <[email protected]>
+Date: Tue, 1 Mar 2016 23:45:08 -0600
+Subject: [PATCH] Entity Origin API
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 9415c699ed2bc2b4237ab5e14cb8316410ac9fa5..7ca86e73d6e206e697578c805f170f08f35f5b5a 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -2193,6 +2193,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ entity.updateDynamicGameEventListener(DynamicGameEventListener::add);
+ entity.inWorld = true; // CraftBukkit - Mark entity as in world
+ entity.valid = true; // CraftBukkit
++ // Paper start - Entity origin API
++ if (entity.getOriginVector() == null) {
++ entity.setOrigin(entity.getBukkitEntity().getLocation());
++ }
++ // Default to current world if unknown, gross assumption but entities rarely change world
++ if (entity.getOriginWorld() == null) {
++ entity.setOrigin(entity.getOriginVector().toLocation(getWorld()));
++ }
++ // Paper end - Entity origin API
+ }
+
+ public void onTrackingEnd(Entity entity) {
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index fdeb762077bf0b87ceb62697f935f611eb9b046b..8825087beb5388876a1206c9704648f3fa98fee2 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -332,7 +332,27 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ public long activatedTick = Integer.MIN_VALUE;
+ public void inactiveTick() { }
+ // Spigot end
++ // Paper start - Entity origin API
++ @javax.annotation.Nullable
++ private org.bukkit.util.Vector origin;
++ @javax.annotation.Nullable
++ private UUID originWorld;
+
++ public void setOrigin(@javax.annotation.Nonnull Location location) {
++ this.origin = location.toVector();
++ this.originWorld = location.getWorld().getUID();
++ }
++
++ @javax.annotation.Nullable
++ public org.bukkit.util.Vector getOriginVector() {
++ return this.origin != null ? this.origin.clone() : null;
++ }
++
++ @javax.annotation.Nullable
++ public UUID getOriginWorld() {
++ return this.originWorld;
++ }
++ // Paper end - Entity origin API
+ public float getBukkitYaw() {
+ return this.yRot;
+ }
+@@ -2293,6 +2313,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ this.bukkitEntity.storeBukkitValues(nbttagcompound);
+ }
+ // CraftBukkit end
++ // Paper start
++ if (this.origin != null) {
++ UUID originWorld = this.originWorld != null ? this.originWorld : this.level != null ? this.level.getWorld().getUID() : null;
++ if (originWorld != null) {
++ nbttagcompound.putUUID("Paper.OriginWorld", originWorld);
++ }
++ nbttagcompound.put("Paper.Origin", this.newDoubleList(origin.getX(), origin.getY(), origin.getZ()));
++ }
++ // Paper end
+ return nbttagcompound;
+ } catch (Throwable throwable) {
+ CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT");
+@@ -2423,6 +2452,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ }
+ // CraftBukkit end
+
++ // Paper start
++ ListTag originTag = nbt.getList("Paper.Origin", net.minecraft.nbt.Tag.TAG_DOUBLE);
++ if (!originTag.isEmpty()) {
++ UUID originWorld = null;
++ if (nbt.contains("Paper.OriginWorld")) {
++ originWorld = nbt.getUUID("Paper.OriginWorld");
++ } else if (this.level != null) {
++ originWorld = this.level.getWorld().getUID();
++ }
++ this.originWorld = originWorld;
++ origin = new org.bukkit.util.Vector(originTag.getDouble(0), originTag.getDouble(1), originTag.getDouble(2));
++ }
++ // Paper end
++
+ } catch (Throwable throwable) {
+ CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT");
+ CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being loaded");
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index 978397e517a6fdb24c7d2b3f242545af07deeab0..ea27931d01c1f3c721b2f7ec12d41ea843fa158a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -964,4 +964,21 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ return this.spigot;
+ }
+ // Spigot end
++
++ // Paper start - entity origin API
++ @Override
++ public Location getOrigin() {
++ Vector originVector = this.getHandle().getOriginVector();
++ if (originVector == null) {
++ return null;
++ }
++ World world = this.getWorld();
++ if (this.getHandle().getOriginWorld() != null) {
++ world = org.bukkit.Bukkit.getWorld(this.getHandle().getOriginWorld());
++ }
++
++ //noinspection ConstantConditions
++ return originVector.toLocation(world);
++ }
++ // Paper end - entity origin API
+ }
diff --git a/patches/server/0037-Prevent-block-entity-and-entity-crashes.patch b/patches/server/0037-Prevent-block-entity-and-entity-crashes.patch
new file mode 100644
index 0000000000..e54880a46c
--- /dev/null
+++ b/patches/server/0037-Prevent-block-entity-and-entity-crashes.patch
@@ -0,0 +1,66 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Tue, 1 Mar 2016 23:52:34 -0600
+Subject: [PATCH] Prevent block entity and entity crashes
+
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 0aa5d3c85b021c552eda139850b48effc3613450..3d2f579cf2b7b96f4ce2588f9655d2287767b01c 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -727,11 +727,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ try {
+ tickConsumer.accept(entity);
+ } catch (Throwable throwable) {
+- CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking entity");
+- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being ticked");
+-
+- entity.fillCrashReportCategory(crashreportsystemdetails);
+- throw new ReportedException(crashreport);
++ // Paper start - Prevent block entity and entity crashes
++ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
++ MinecraftServer.LOGGER.error(msg, throwable);
++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ // Paper end - Prevent block entity and entity crashes
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+index 1fa90dbf0e431d1f69ab46aa3dc200f09cfe7536..4c5ff6009f72ec3e56a1f440904c472fb2a4138a 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -258,7 +258,12 @@ public abstract class BlockEntity {
+ public void fillCrashReportCategory(CrashReportCategory crashReportSection) {
+ crashReportSection.setDetail("Name", this::getNameForReporting);
+ if (this.level != null) {
+- CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.getBlockState());
++ // Paper start - Prevent block entity and entity crashes
++ BlockState block = this.getBlockState();
++ if (block != null) {
++ CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, block);
++ }
++ // Paper end - Prevent block entity and entity crashes
+ CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.level.getBlockState(this.worldPosition));
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index b215ed941197f5aa0e26cd50a420500dcdc17fbb..daa2b3f67765bc340404747b3cffea5604830515 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -988,11 +988,11 @@ public class LevelChunk extends ChunkAccess {
+
+ gameprofilerfiller.pop();
+ } catch (Throwable throwable) {
+- CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking block entity");
+- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block entity being ticked");
+-
+- this.blockEntity.fillCrashReportCategory(crashreportsystemdetails);
+- throw new ReportedException(crashreport);
++ // Paper start - Prevent block entity and entity crashes
++ final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
++ net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable);
++ LevelChunk.this.removeBlockEntity(this.getPos());
++ // Paper end - Prevent block entity and entity crashes
+ // Spigot start
+ }
+ }
diff --git a/patches/server/0038-Configurable-top-of-nether-void-damage.patch b/patches/server/0038-Configurable-top-of-nether-void-damage.patch
new file mode 100644
index 0000000000..61ac8b5be8
--- /dev/null
+++ b/patches/server/0038-Configurable-top-of-nether-void-damage.patch
@@ -0,0 +1,49 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 1 Mar 2016 23:58:50 -0600
+Subject: [PATCH] Configurable top of nether void damage
+
+Co-authored-by: Jake Potrebic <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index 8825087beb5388876a1206c9704648f3fa98fee2..61f7461150c7354a641b86ce42a2b41460d3a45d 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -725,7 +725,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ }
+
+ public void checkBelowWorld() {
+- if (this.getY() < (double) (this.level().getMinY() - 64)) {
++ // Paper start - Configurable nether ceiling damage
++ if (this.getY() < (double) (this.level.getMinY() - 64) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER
++ && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v)
++ && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) {
++ // Paper end - Configurable nether ceiling damage
+ this.onBelowWorld();
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+index 36accb58ed269a129f92d2b64f5a0b14416de735..355f1ce10f9564c7c0be505a5af849e0428fec17 100644
+--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+@@ -58,7 +58,7 @@ public class PortalForcer {
+ }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos);
+
+ Objects.requireNonNull(worldborder);
+- return stream.filter(worldborder::isWithinBounds).filter((blockposition1) -> {
++ return stream.filter(worldborder::isWithinBounds).filter(pos -> !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))).filter((blockposition1) -> { // Paper - Configurable nether ceiling damage
+ return this.level.getBlockState(blockposition1).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
+ }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error
+ return blockposition1.distSqr(blockposition);
+@@ -79,6 +79,11 @@ public class PortalForcer {
+ BlockPos blockposition2 = null;
+ WorldBorder worldborder = this.level.getWorldBorder();
+ int i = Math.min(this.level.getMaxY(), this.level.getMinY() + this.level.getLogicalHeight() - 1);
++ // Paper start - Configurable nether ceiling damage; make sure the max height doesn't exceed the void damage height
++ if (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.enabled()) {
++ i = Math.min(i, this.level.paperConfig().environment.netherCeilingVoidDamageHeight.intValue() - 1);
++ }
++ // Paper end - Configurable nether ceiling damage
+ boolean flag = true;
+ BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable();
+ Iterator iterator = BlockPos.spiralAround(blockposition, createRadius, Direction.EAST, Direction.SOUTH).iterator(); // CraftBukkit
diff --git a/patches/server/0039-Check-online-mode-before-converting-and-renaming-pla.patch b/patches/server/0039-Check-online-mode-before-converting-and-renaming-pla.patch
new file mode 100644
index 0000000000..70183b1722
--- /dev/null
+++ b/patches/server/0039-Check-online-mode-before-converting-and-renaming-pla.patch
@@ -0,0 +1,19 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Wed, 2 Mar 2016 00:03:55 -0600
+Subject: [PATCH] Check online mode before converting and renaming player data
+
+
+diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+index 79222a04ef104bf2eed85684479384b7c350ea19..cd013567dd6224c86c0f1813d8a3d5fb7b8cabb5 100644
+--- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
++++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
+@@ -78,7 +78,7 @@ public class PlayerDataStorage {
+ File file1 = new File(file, s1 + s);
+ // Spigot Start
+ boolean usingWrongFile = false;
+- if ( !file1.exists() )
++ if ( org.bukkit.Bukkit.getOnlineMode() && !file1.exists() ) // Paper - Check online mode first
+ {
+ file1 = new File( file, java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + name ).getBytes( java.nio.charset.StandardCharsets.UTF_8 ) ).toString() + s );
+ if ( file1.exists() )
diff --git a/patches/server/0040-Add-more-entities-to-activation-range-ignore-list.patch b/patches/server/0040-Add-more-entities-to-activation-range-ignore-list.patch
new file mode 100644
index 0000000000..f7bf86db8b
--- /dev/null
+++ b/patches/server/0040-Add-more-entities-to-activation-range-ignore-list.patch
@@ -0,0 +1,20 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Wed, 2 Mar 2016 00:32:25 -0600
+Subject: [PATCH] Add more entities to activation range ignore list
+
+
+diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
+index 0338ceaddc1a0921f5f8796d5eac75c301bafac2..3184090cfa1ce6ec01686a7651be01cff49f5951 100644
+--- a/src/main/java/org/spigotmc/ActivationRange.java
++++ b/src/main/java/org/spigotmc/ActivationRange.java
+@@ -92,6 +92,9 @@ public class ActivationRange
+ || entity instanceof AbstractHurtingProjectile
+ || entity instanceof LightningBolt
+ || entity instanceof PrimedTnt
++ || entity instanceof net.minecraft.world.entity.item.FallingBlockEntity // Paper - Always tick falling blocks
++ || entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart // Paper
++ || entity instanceof net.minecraft.world.entity.vehicle.AbstractBoat // Paper
+ || entity instanceof EndCrystal
+ || entity instanceof FireworkRocketEntity
+ || entity instanceof ThrownTrident )
diff --git a/patches/server/0041-Configurable-end-credits.patch b/patches/server/0041-Configurable-end-credits.patch
new file mode 100644
index 0000000000..17ff16662c
--- /dev/null
+++ b/patches/server/0041-Configurable-end-credits.patch
@@ -0,0 +1,18 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: DoctorDark <[email protected]>
+Date: Wed, 16 Mar 2016 02:21:39 -0500
+Subject: [PATCH] Configurable end credits
+
+
+diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java
+index 4e560e290669fd62e373e5fa3515404f6c222336..8887d35d188510cf10da3dc46b0b56373ac346bd 100644
+--- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java
+@@ -76,6 +76,7 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal {
+ if (!world.isClientSide && world.dimension() == Level.END && entity instanceof ServerPlayer) {
+ ServerPlayer entityplayer = (ServerPlayer) entity;
+
++ if (world.paperConfig().misc.disableEndCredits) entityplayer.seenCredits = true; // Paper - Option to disable end credits
+ if (!entityplayer.seenCredits) {
+ entityplayer.showEndCredits();
+ return;
diff --git a/patches/server/0042-Fix-lag-from-explosions-processing-dead-entities.patch b/patches/server/0042-Fix-lag-from-explosions-processing-dead-entities.patch
new file mode 100644
index 0000000000..1c5b19ed1f
--- /dev/null
+++ b/patches/server/0042-Fix-lag-from-explosions-processing-dead-entities.patch
@@ -0,0 +1,19 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Iceee <[email protected]>
+Date: Wed, 2 Mar 2016 01:39:52 -0600
+Subject: [PATCH] Fix lag from explosions processing dead entities
+
+
+diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+index 9ef8b671d5f969c71ac00ec1b49874ac05ca84ac..67685dfa8eb4a74e70ef982291a55d145d6abc66 100644
+--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java
++++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+@@ -185,7 +185,7 @@ public class ServerExplosion implements Explosion {
+ int l = Mth.floor(this.center.y + (double) f + 1.0D);
+ int i1 = Mth.floor(this.center.z - (double) f - 1.0D);
+ int j1 = Mth.floor(this.center.z + (double) f + 1.0D);
+- List<Entity> list = this.level.getEntities(this.source, new AABB((double) i, (double) k, (double) i1, (double) j, (double) l, (double) j1));
++ List<Entity> list = this.level.getEntities(this.source, new AABB((double) i, (double) k, (double) i1, (double) j, (double) l, (double) j1), (com.google.common.base.Predicate<Entity>) entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities
+ Iterator iterator = list.iterator();
+
+ while (iterator.hasNext()) {
diff --git a/patches/server/0043-Optimize-explosions.patch b/patches/server/0043-Optimize-explosions.patch
new file mode 100644
index 0000000000..a3cabb44da
--- /dev/null
+++ b/patches/server/0043-Optimize-explosions.patch
@@ -0,0 +1,134 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Byteflux <[email protected]>
+Date: Wed, 2 Mar 2016 11:59:48 -0600
+Subject: [PATCH] Optimize explosions
+
+The process of determining an entity's exposure from explosions can be
+expensive when there are hundreds or more entities in range.
+
+This patch adds a per-tick cache that is used for storing and retrieving
+an entity's exposure during an explosion.
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index f70468b2cc4a782d3d7cc7854192fbbc3ad3020f..9c26725e12adb2d17b9fa27f632fbad02e904c9a 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -1611,6 +1611,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ gameprofilerfiller.pop();
+ gameprofilerfiller.pop();
++ worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions
+ }
+
+ gameprofilerfiller.popPush("connection");
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 3d2f579cf2b7b96f4ce2588f9655d2287767b01c..7e6680d0618173e23d323b4b29f0a2fc966789e7 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -169,6 +169,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ private org.spigotmc.TickLimiter entityLimiter;
+ private org.spigotmc.TickLimiter tileLimiter;
+ private int tileTickPosition;
++ public final Map<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
+
+ public CraftWorld getWorld() {
+ return this.world;
+diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+index 67685dfa8eb4a74e70ef982291a55d145d6abc66..bbe50f60e653e60c844ca6688e781fa69d67905a 100644
+--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java
++++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+@@ -206,7 +206,7 @@ public class ServerExplosion implements Explosion {
+ d3 /= d4;
+ boolean flag = this.damageCalculator.shouldDamageEntity(this, entity);
+ float f1 = this.damageCalculator.getKnockbackMultiplier(entity);
+- float f2 = !flag && f1 == 0.0F ? 0.0F : ServerExplosion.getSeenPercent(this.center, entity);
++ float f2 = !flag && f1 == 0.0F ? 0.0F : this.getBlockDensity(this.center, entity); // Paper - Optimize explosions
+
+ if (flag) {
+ // CraftBukkit start
+@@ -487,4 +487,85 @@ public class ServerExplosion implements Explosion {
+
+ }
+ }
++
++ // Paper start - Optimize explosions
++ private float getBlockDensity(Vec3 vec3d, Entity entity) {
++ if (!this.level.paperConfig().environment.optimizeExplosions) {
++ return getSeenPercent(vec3d, entity);
++ }
++ CacheKey key = new CacheKey(this, entity.getBoundingBox());
++ Float blockDensity = this.level.explosionDensityCache.get(key);
++ if (blockDensity == null) {
++ blockDensity = getSeenPercent(vec3d, entity);
++ this.level.explosionDensityCache.put(key, blockDensity);
++ }
++
++ return blockDensity;
++ }
++
++ static class CacheKey {
++ private final Level world;
++ private final double posX, posY, posZ;
++ private final double minX, minY, minZ;
++ private final double maxX, maxY, maxZ;
++
++ public CacheKey(Explosion explosion, AABB aabb) {
++ this.world = explosion.level();
++ this.posX = explosion.center().x;
++ this.posY = explosion.center().y;
++ this.posZ = explosion.center().z;
++ this.minX = aabb.minX;
++ this.minY = aabb.minY;
++ this.minZ = aabb.minZ;
++ this.maxX = aabb.maxX;
++ this.maxY = aabb.maxY;
++ this.maxZ = aabb.maxZ;
++ }
++
++ @Override
++ public boolean equals(Object o) {
++ if (this == o) return true;
++ if (o == null || getClass() != o.getClass()) return false;
++
++ CacheKey cacheKey = (CacheKey) o;
++
++ if (Double.compare(cacheKey.posX, posX) != 0) return false;
++ if (Double.compare(cacheKey.posY, posY) != 0) return false;
++ if (Double.compare(cacheKey.posZ, posZ) != 0) return false;
++ if (Double.compare(cacheKey.minX, minX) != 0) return false;
++ if (Double.compare(cacheKey.minY, minY) != 0) return false;
++ if (Double.compare(cacheKey.minZ, minZ) != 0) return false;
++ if (Double.compare(cacheKey.maxX, maxX) != 0) return false;
++ if (Double.compare(cacheKey.maxY, maxY) != 0) return false;
++ if (Double.compare(cacheKey.maxZ, maxZ) != 0) return false;
++ return world.equals(cacheKey.world);
++ }
++
++ @Override
++ public int hashCode() {
++ int result;
++ long temp;
++ result = world.hashCode();
++ temp = Double.doubleToLongBits(posX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(posY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(posZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ return result;
++ }
++ }
++ // Paper end
+ }
diff --git a/patches/server/0044-Disable-explosion-knockback.patch b/patches/server/0044-Disable-explosion-knockback.patch
new file mode 100644
index 0000000000..aba45775df
--- /dev/null
+++ b/patches/server/0044-Disable-explosion-knockback.patch
@@ -0,0 +1,28 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sudzzy <[email protected]>
+Date: Wed, 2 Mar 2016 14:48:03 -0600
+Subject: [PATCH] Disable explosion knockback
+
+
+diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+index bbe50f60e653e60c844ca6688e781fa69d67905a..e816e54daff781d54a94a007df5c59051347739d 100644
+--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java
++++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java
+@@ -246,7 +246,7 @@ public class ServerExplosion implements Explosion {
+ if (entity instanceof LivingEntity) {
+ LivingEntity entityliving = (LivingEntity) entity;
+
+- d6 = d5 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE));
++ d6 = entity instanceof Player && this.level.paperConfig().environment.disableExplosionKnockback ? 0 : d5 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE)); // Paper
+ } else {
+ d6 = d5;
+ }
+@@ -271,7 +271,7 @@ public class ServerExplosion implements Explosion {
+ if (entity instanceof Player) {
+ Player entityhuman = (Player) entity;
+
+- if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying)) {
++ if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying) && !level.paperConfig().environment.disableExplosionKnockback) { // Paper - Option to disable explosion knockback
+ this.hitPlayers.put(entityhuman, vec3d);
+ }
+ }
diff --git a/patches/server/0045-Disable-thunder.patch b/patches/server/0045-Disable-thunder.patch
new file mode 100644
index 0000000000..7bb69a9201
--- /dev/null
+++ b/patches/server/0045-Disable-thunder.patch
@@ -0,0 +1,19 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sudzzy <[email protected]>
+Date: Wed, 2 Mar 2016 14:52:43 -0600
+Subject: [PATCH] Disable thunder
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 7ca86e73d6e206e697578c805f170f08f35f5b5a..e3c894619d64352896b5290d3b7592611bc3164d 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -595,7 +595,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("thunder");
+- if (flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot
++ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
+ BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
+
+ if (this.isRainingAt(blockposition)) {
diff --git a/patches/server/0046-Disable-ice-and-snow.patch b/patches/server/0046-Disable-ice-and-snow.patch
new file mode 100644
index 0000000000..d7858e36a8
--- /dev/null
+++ b/patches/server/0046-Disable-ice-and-snow.patch
@@ -0,0 +1,24 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sudzzy <[email protected]>
+Date: Wed, 2 Mar 2016 14:57:24 -0600
+Subject: [PATCH] Disable ice and snow
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index e3c894619d64352896b5290d3b7592611bc3164d..db57ac5086f862833057a06ff1253934ef46230d 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -625,11 +625,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ gameprofilerfiller.popPush("iceandsnow");
+
++ if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
+ for (int l = 0; l < randomTickSpeed; ++l) {
+ if (this.random.nextInt(48) == 0) {
+ this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15));
+ }
+ }
++ } // Paper - Option to disable ice and snow
+
+ gameprofilerfiller.popPush("tickBlocks");
+ if (randomTickSpeed > 0) {
diff --git a/patches/server/0047-Configurable-mob-spawner-tick-rate.patch b/patches/server/0047-Configurable-mob-spawner-tick-rate.patch
new file mode 100644
index 0000000000..a3dc9ff7d3
--- /dev/null
+++ b/patches/server/0047-Configurable-mob-spawner-tick-rate.patch
@@ -0,0 +1,39 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sudzzy <[email protected]>
+Date: Wed, 2 Mar 2016 15:03:53 -0600
+Subject: [PATCH] Configurable mob spawner tick rate
+
+
+diff --git a/src/main/java/net/minecraft/world/level/BaseSpawner.java b/src/main/java/net/minecraft/world/level/BaseSpawner.java
+index 366661561544f8e99f238583259991e9fcbab8af..7b918001d36a8f14ed0d3ee4d6783588f48eb78f 100644
+--- a/src/main/java/net/minecraft/world/level/BaseSpawner.java
++++ b/src/main/java/net/minecraft/world/level/BaseSpawner.java
+@@ -49,6 +49,7 @@ public abstract class BaseSpawner {
+ public int maxNearbyEntities = 6;
+ public int requiredPlayerRange = 16;
+ public int spawnRange = 4;
++ private int tickDelay = 0; // Paper - Configurable mob spawner tick rate
+
+ public BaseSpawner() {}
+
+@@ -83,13 +84,18 @@ public abstract class BaseSpawner {
+ }
+
+ public void serverTick(ServerLevel world, BlockPos pos) {
++ // Paper start - Configurable mob spawner tick rate
++ if (spawnDelay > 0 && --tickDelay > 0) return;
++ tickDelay = world.paperConfig().tickRates.mobSpawner;
++ if (tickDelay == -1) { return; } // If disabled
++ // Paper end - Configurable mob spawner tick rate
+ if (this.isNearPlayer(world, pos)) {
+- if (this.spawnDelay == -1) {
++ if (this.spawnDelay < -tickDelay) { // Paper - Configurable mob spawner tick rate
+ this.delay(world, pos);
+ }
+
+ if (this.spawnDelay > 0) {
+- --this.spawnDelay;
++ this.spawnDelay -= tickDelay; // Paper - Configurable mob spawner tick rate
+ } else {
+ boolean flag = false;
+ RandomSource randomsource = world.getRandom();
diff --git a/patches/server/0048-Use-null-Locale-by-default.patch b/patches/server/0048-Use-null-Locale-by-default.patch
new file mode 100644
index 0000000000..b75495a16e
--- /dev/null
+++ b/patches/server/0048-Use-null-Locale-by-default.patch
@@ -0,0 +1,53 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Isaac Moore <[email protected]>
+Date: Tue, 19 Apr 2016 14:09:31 -0500
+Subject: [PATCH] Use null Locale by default
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 3228c664925b1214aae0a693b93fdbe4d6698aa0..01ae7e9847602d489c34f060ff132a1f5f9781a3 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -259,7 +259,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ private int levitationStartTime;
+ private boolean disconnected;
+ private int requestedViewDistance;
+- public String language = "en_us"; // CraftBukkit - default
++ public String language = null; // CraftBukkit - default // Paper - default to null
+ public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
+ @Nullable
+ private Vec3 startingToFallPosition;
+@@ -319,7 +319,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ this.canChatColor = true;
+ this.lastActionTime = Util.getMillis();
+ this.requestedViewDistance = 2;
+- this.language = "en_us";
++ this.language = null; // Paper - default to null
+ this.lastSectionPos = SectionPos.of(0, 0, 0);
+ this.chunkTrackingView = ChunkTrackingView.EMPTY;
+ this.respawnDimension = Level.OVERWORLD;
+@@ -2295,7 +2295,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ PlayerChangedMainHandEvent event = new PlayerChangedMainHandEvent(this.getBukkitEntity(), this.getMainArm() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT);
+ this.server.server.getPluginManager().callEvent(event);
+ }
+- if (!this.language.equals(clientOptions.language())) {
++ if (this.language == null || !this.language.equals(clientOptions.language())) { // Paper
+ PlayerLocaleChangeEvent event = new PlayerLocaleChangeEvent(this.getBukkitEntity(), clientOptions.language());
+ this.server.server.getPluginManager().callEvent(event);
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index fcaac0479ef0259d2271de0cd12752833873a1f6..38dd2a6a6d92e0f89a8a141455382d49afa441dd 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2435,7 +2435,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public String getLocale() {
+- return this.getHandle().language;
++ // Paper start - Locale change event
++ final String locale = this.getHandle().language;
++ return locale != null ? locale : "en_us";
++ // Paper end
+ }
+
+ // Paper start
diff --git a/patches/server/0049-Add-BeaconEffectEvent.patch b/patches/server/0049-Add-BeaconEffectEvent.patch
new file mode 100644
index 0000000000..ce80109f25
--- /dev/null
+++ b/patches/server/0049-Add-BeaconEffectEvent.patch
@@ -0,0 +1,66 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Byteflux <[email protected]>
+Date: Wed, 2 Mar 2016 23:30:53 -0600
+Subject: [PATCH] Add BeaconEffectEvent
+
+
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
+index 94fcd67edd81120d56478ffd30f3c1d7dee955e6..8332296663b845df1d09d403b49a4769b2d54afc 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
+@@ -1,5 +1,6 @@
+ package net.minecraft.world.level.block.entity;
+
++import com.destroystokyo.paper.event.block.BeaconEffectEvent;
+ import com.google.common.collect.ImmutableList;
+ import com.google.common.collect.Lists;
+ import java.util.Collection;
+@@ -46,6 +47,7 @@ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.levelgen.Heightmap;
+ import net.minecraft.world.phys.AABB;
+ // CraftBukkit start
++import org.bukkit.craftbukkit.event.CraftEventFactory;
+ import org.bukkit.craftbukkit.potion.CraftPotionUtil;
+ import org.bukkit.potion.PotionEffect;
+ // CraftBukkit end
+@@ -295,15 +297,23 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name
+ }
+ }
+
+- private static void applyEffect(List list, @Nullable Holder<MobEffect> holder, int j, int b0) {
+- {
++ private static void applyEffect(List list, @Nullable Holder<MobEffect> holder, int j, int b0, boolean isPrimary, BlockPos worldPosition) { // Paper - BeaconEffectEvent
++ if (!list.isEmpty()) { // Paper - BeaconEffectEvent
+ Iterator iterator = list.iterator();
+
+ Player entityhuman;
++ // Paper start - BeaconEffectEvent
++ org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(((Player) list.get(0)).level(), worldPosition);
++ PotionEffect effect = CraftPotionUtil.toBukkit(new MobEffectInstance(holder, j, b0, true, true));
++ // Paper end - BeaconEffectEvent
+
+ while (iterator.hasNext()) {
+- entityhuman = (Player) iterator.next();
+- entityhuman.addEffect(new MobEffectInstance(holder, j, b0, true, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON);
++ // Paper start - BeaconEffectEvent
++ entityhuman = (ServerPlayer) iterator.next();
++ BeaconEffectEvent event = new BeaconEffectEvent(block, effect, (org.bukkit.entity.Player) entityhuman.getBukkitEntity(), isPrimary);
++ if (CraftEventFactory.callEvent(event).isCancelled()) continue;
++ entityhuman.addEffect(new MobEffectInstance(CraftPotionUtil.fromBukkit(event.getEffect())), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON);
++ // Paper end - BeaconEffectEvent
+ }
+ }
+ }
+@@ -326,10 +336,10 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name
+ int j = BeaconBlockEntity.getLevel(beaconLevel);
+ List list = BeaconBlockEntity.getHumansInRange(world, pos, beaconLevel);
+
+- BeaconBlockEntity.applyEffect(list, primaryEffect, j, b0);
++ BeaconBlockEntity.applyEffect(list, primaryEffect, j, b0, true, pos); // Paper - BeaconEffectEvent
+
+ if (BeaconBlockEntity.hasSecondaryEffect(beaconLevel, primaryEffect, secondaryEffect)) {
+- BeaconBlockEntity.applyEffect(list, secondaryEffect, j, 0);
++ BeaconBlockEntity.applyEffect(list, secondaryEffect, j, 0, false, pos); // Paper - BeaconEffectEvent
+ }
+ }
+
diff --git a/patches/server/0050-Configurable-container-update-tick-rate.patch b/patches/server/0050-Configurable-container-update-tick-rate.patch
new file mode 100644
index 0000000000..e0cfe6d6ee
--- /dev/null
+++ b/patches/server/0050-Configurable-container-update-tick-rate.patch
@@ -0,0 +1,31 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Sudzzy <[email protected]>
+Date: Wed, 2 Mar 2016 23:34:44 -0600
+Subject: [PATCH] Configurable container update tick rate
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 01ae7e9847602d489c34f060ff132a1f5f9781a3..7e5fb0abe6cb4f10c41dedb2076e70bd9ace5430 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -293,6 +293,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ private final CommandSource commandSource;
+ private int containerCounter;
+ public boolean wonGame;
++ private int containerUpdateDelay; // Paper - Configurable container update tick rate
+
+ // CraftBukkit start
+ public CraftPlayer.TransferCookieConnection transferCookieConnection;
+@@ -922,7 +923,11 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ --this.invulnerableTime;
+ }
+
+- this.containerMenu.broadcastChanges();
++ if (--this.containerUpdateDelay <= 0) {
++ this.containerMenu.broadcastChanges();
++ this.containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
++ }
++ // Paper end - Configurable container update tick rate
+ if (!this.containerMenu.stillValid(this)) {
+ this.closeContainer();
+ this.containerMenu = this.inventoryMenu;
diff --git a/patches/server/0051-Use-UserCache-for-player-heads.patch b/patches/server/0051-Use-UserCache-for-player-heads.patch
new file mode 100644
index 0000000000..b7c3f11a27
--- /dev/null
+++ b/patches/server/0051-Use-UserCache-for-player-heads.patch
@@ -0,0 +1,25 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Techcable <[email protected]>
+Date: Wed, 2 Mar 2016 23:42:37 -0600
+Subject: [PATCH] Use UserCache for player heads
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java
+index 43609c99347c2e3e6ee9a1b8926b32b458781fba..7181d81c231908f208b48a29f918136cb143f476 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSkull.java
+@@ -172,7 +172,13 @@ class CraftMetaSkull extends CraftMetaItem implements SkullMeta {
+ if (name == null) {
+ this.setProfile(null);
+ } else {
+- this.setProfile(new ResolvableProfile(new GameProfile(Util.NIL_UUID, name)));
++ // Paper start - Use Online Players Skull
++ GameProfile newProfile = null;
++ net.minecraft.server.level.ServerPlayer player = net.minecraft.server.MinecraftServer.getServer().getPlayerList().getPlayerByName(name);
++ if (player != null) newProfile = player.getGameProfile();
++ if (newProfile == null) newProfile = new GameProfile(Util.NIL_UUID, name);
++ this.setProfile(new ResolvableProfile(newProfile));
++ // Paper end
+ }
+
+ return true;
diff --git a/patches/server/0052-Disable-spigot-tick-limiters.patch b/patches/server/0052-Disable-spigot-tick-limiters.patch
new file mode 100644
index 0000000000..1a8fb27a07
--- /dev/null
+++ b/patches/server/0052-Disable-spigot-tick-limiters.patch
@@ -0,0 +1,21 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Wed, 2 Mar 2016 23:45:17 -0600
+Subject: [PATCH] Disable spigot tick limiters
+
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 7e6680d0618173e23d323b4b29f0a2fc966789e7..d8621149add06021d594415e079771b6fd57cc3f 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -702,9 +702,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ boolean flag = this.tickRateManager().runsNormally();
+
+ int tilesThisCycle = 0;
+- for (this.tileLimiter.initTick();
+- tilesThisCycle < this.blockEntityTickers.size() && (tilesThisCycle % 10 != 0 || this.tileLimiter.shouldContinue());
+- this.tileTickPosition++, tilesThisCycle++) {
++ for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
+ this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
+ TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition);
+ // Spigot end
diff --git a/patches/server/0053-Fix-spawn-location-event-changing-location.patch b/patches/server/0053-Fix-spawn-location-event-changing-location.patch
new file mode 100644
index 0000000000..cc20e90b16
--- /dev/null
+++ b/patches/server/0053-Fix-spawn-location-event-changing-location.patch
@@ -0,0 +1,24 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Steve Anton <[email protected]>
+Date: Thu, 3 Mar 2016 00:09:38 -0600
+Subject: [PATCH] Fix spawn location event changing location
+
+== AT ==
+public net.minecraft.world.entity.Entity setRot(FF)V
+
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 8daa027a94602d7d556cf4fbfc8fcd97caf6bd98..7782f26764ef79968b1e2f5e1f27f1162ed122de 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -234,7 +234,10 @@ public abstract class PlayerList {
+
+ player.spawnIn(worldserver1);
+ player.gameMode.setLevel((ServerLevel) player.level());
+- player.absMoveTo(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
++ // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world)
++ player.setPosRaw(loc.getX(), loc.getY(), loc.getZ());
++ player.setRot(loc.getYaw(), loc.getPitch());
++ // Paper end - set raw so we aren't fully joined to the world
+ // Spigot end
+
+ // CraftBukkit - Moved message to after join
diff --git a/patches/server/0054-Configurable-Disabling-Cat-Chest-Detection.patch b/patches/server/0054-Configurable-Disabling-Cat-Chest-Detection.patch
new file mode 100644
index 0000000000..d7b43107a7
--- /dev/null
+++ b/patches/server/0054-Configurable-Disabling-Cat-Chest-Detection.patch
@@ -0,0 +1,23 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 3 Mar 2016 01:13:45 -0600
+Subject: [PATCH] Configurable Disabling Cat Chest Detection
+
+Offers a gameplay feature to stop cats from blocking chests
+
+diff --git a/src/main/java/net/minecraft/world/level/block/ChestBlock.java b/src/main/java/net/minecraft/world/level/block/ChestBlock.java
+index accdc4200bf0b6f1064d973ec41002a8cdde6b0b..252d2ab67c125ae075689fa779477eb698c20a15 100644
+--- a/src/main/java/net/minecraft/world/level/block/ChestBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/ChestBlock.java
+@@ -343,6 +343,11 @@ public class ChestBlock extends AbstractChestBlock<ChestBlockEntity> implements
+ }
+
+ private static boolean isCatSittingOnChest(LevelAccessor world, BlockPos pos) {
++ // Paper start - Option to disable chest cat detection
++ if (world.getMinecraftWorld().paperConfig().entities.behavior.disableChestCatDetection) {
++ return false;
++ }
++ // Paper end - Option to disable chest cat detection
+ List<Cat> list = world.getEntitiesOfClass(Cat.class, new AABB((double) pos.getX(), (double) (pos.getY() + 1), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 2), (double) (pos.getZ() + 1)));
+
+ if (!list.isEmpty()) {
diff --git a/patches/server/0055-Improve-Player-chat-API-handling.patch b/patches/server/0055-Improve-Player-chat-API-handling.patch
new file mode 100644
index 0000000000..7d96e48efd
--- /dev/null
+++ b/patches/server/0055-Improve-Player-chat-API-handling.patch
@@ -0,0 +1,78 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 3 Mar 2016 01:17:12 -0600
+Subject: [PATCH] Improve Player chat API handling
+
+Properly split up the chat and command handling to reflect the server now
+having separate packets for both, and the client always using the correct packet. Text
+from a chat packet should never be parsed into a command, even if it starts with the `/`
+character.
+
+Add a missing async catcher and improve Spigot's async catcher error message.
+
+== AT ==
+public net.minecraft.server.network.ServerGamePacketListenerImpl isChatMessageIllegal(Ljava/lang/String;)Z
+
+Co-authored-by: Jake Potrebic <[email protected]>
+Co-authored-by: SoSeDiK <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index f21de202d7c932832fca9402a17a13e336aa36c8..97b776e384e8ce064ea9bb93fe24d902ff2d8817 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -2099,7 +2099,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+ OutgoingChatMessage outgoing = OutgoingChatMessage.create(original);
+
+- if (!async && s.startsWith("/")) {
++ if (false && !async && s.startsWith("/")) { // Paper - Don't handle commands in chat logic
+ this.handleCommand(s);
+ } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
+ // Do nothing, this is coming from a plugin
+@@ -2187,6 +2187,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ private void handleCommand(String s) {
++ org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + s); // Paper - Add async catcher
+ if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot
+ this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s);
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index e4335bfc98272c5499651977625e1f0ca671fbec..a0a0fa7de47402f1820618da5c6582dd894676cd 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -936,7 +936,7 @@ public final class CraftServer implements Server {
+ public boolean dispatchCommand(CommandSender sender, String commandLine) {
+ Preconditions.checkArgument(sender != null, "sender cannot be null");
+ Preconditions.checkArgument(commandLine != null, "commandLine cannot be null");
+- org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot
++ org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + commandLine); // Spigot // Paper - Include command in error message
+
+ if (this.commandMap.dispatch(sender, commandLine)) {
+ return true;
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 38dd2a6a6d92e0f89a8a141455382d49afa441dd..aa5755bd263c7682e41eeacbde28934b40796cf5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -563,7 +563,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ if (this.getHandle().connection == null) return;
+
+- this.getHandle().connection.chat(msg, PlayerChatMessage.system(msg), false);
++ // Paper start - Improve chat handling
++ if (ServerGamePacketListenerImpl.isChatMessageIllegal(msg)) {
++ this.getHandle().connection.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
++ } else {
++ if (msg.startsWith("/")) {
++ this.getHandle().connection.handleCommand(msg);
++ } else {
++ final PlayerChatMessage playerChatMessage = PlayerChatMessage.system(msg).withUnsignedContent(Component.literal(msg));
++ // TODO chat decorating
++ // TODO text filtering
++ this.getHandle().connection.chat(msg, playerChatMessage, false);
++ }
++ }
++ // Paper end - Improve chat handling
+ }
+
+ @Override
diff --git a/patches/server/0056-All-chunks-are-slime-spawn-chunks-toggle.patch b/patches/server/0056-All-chunks-are-slime-spawn-chunks-toggle.patch
new file mode 100644
index 0000000000..454765e6e5
--- /dev/null
+++ b/patches/server/0056-All-chunks-are-slime-spawn-chunks-toggle.patch
@@ -0,0 +1,32 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: vemacs <[email protected]>
+Date: Thu, 3 Mar 2016 01:19:22 -0600
+Subject: [PATCH] All chunks are slime spawn chunks toggle
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/monster/Slime.java b/src/main/java/net/minecraft/world/entity/monster/Slime.java
+index 7b45d1b706550d7d0a0267f30fb0b86813edfeb3..131fce812eb0dcdebab02b529ed18e81eb1861eb 100644
+--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java
++++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java
+@@ -342,7 +342,7 @@ public class Slime extends Mob implements Enemy {
+ }
+
+ ChunkPos chunkcoordintpair = new ChunkPos(pos);
+- boolean flag = WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), world.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot
++ boolean flag = world.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), world.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot // Paper
+
+ if (random.nextInt(10) == 0 && flag && pos.getY() < 40) {
+ return checkMobSpawnRules(type, world, spawnReason, pos, random);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+index d54e0ab739ad33b8222d9ea2766e2a893154ee26..d3b2d71a570b90d58dd7d00ce625b0169c106190 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+@@ -218,7 +218,7 @@ public class CraftChunk implements Chunk {
+ @Override
+ public boolean isSlimeChunk() {
+ // 987234911L is deterimined in EntitySlime when seeing if a slime can spawn in a chunk
+- return WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), this.worldServer.spigotConfig.slimeSeed).nextInt(10) == 0;
++ return this.worldServer.paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), worldServer.spigotConfig.slimeSeed).nextInt(10) == 0; // Paper
+ }
+
+ @Override
diff --git a/patches/server/0057-Expose-server-CommandMap.patch b/patches/server/0057-Expose-server-CommandMap.patch
new file mode 100644
index 0000000000..4eafe19d32
--- /dev/null
+++ b/patches/server/0057-Expose-server-CommandMap.patch
@@ -0,0 +1,18 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kashike <[email protected]>
+Date: Thu, 3 Mar 2016 02:15:57 -0600
+Subject: [PATCH] Expose server CommandMap
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index a0a0fa7de47402f1820618da5c6582dd894676cd..e8528943c4c2f5c03a6a83d68133bace86b7901a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -2182,6 +2182,7 @@ public final class CraftServer implements Server {
+ return this.helpMap;
+ }
+
++ @Override // Paper - add override
+ public SimpleCommandMap getCommandMap() {
+ return this.commandMap;
+ }
diff --git a/patches/server/0058-Be-a-bit-more-informative-in-maxHealth-exception.patch b/patches/server/0058-Be-a-bit-more-informative-in-maxHealth-exception.patch
new file mode 100644
index 0000000000..583ea362ac
--- /dev/null
+++ b/patches/server/0058-Be-a-bit-more-informative-in-maxHealth-exception.patch
@@ -0,0 +1,24 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kashike <[email protected]>
+Date: Thu, 3 Mar 2016 02:18:39 -0600
+Subject: [PATCH] Be a bit more informative in maxHealth exception
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+index e92bd027d1302ab784625a4e148e74053157b5c8..b34829d780d9f695b8109f4baed7cc251f2b6fdc 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+@@ -108,7 +108,12 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
+ @Override
+ public void setHealth(double health) {
+ health = (float) health;
+- Preconditions.checkArgument(health >= 0 && health <= this.getMaxHealth(), "Health value (%s) must be between 0 and %s", health, this.getMaxHealth());
++ // Paper start - Be more informative
++ Preconditions.checkArgument(health >= 0 && health <= this.getMaxHealth(),
++ "Health value (%s) must be between 0 and %s. (attribute base value: %s%s)",
++ health, this.getMaxHealth(), this.getHandle().getAttribute(Attributes.MAX_HEALTH).getBaseValue(), this instanceof CraftPlayer ? ", player: " + this.getName() : ""
++ );
++ // Paper end
+
+ // during world generation, we don't want to run logic for dropping items and xp
+ if (this.getHandle().generation && health == 0) {
diff --git a/patches/server/0059-Player-Tab-List-and-Title-APIs.patch b/patches/server/0059-Player-Tab-List-and-Title-APIs.patch
new file mode 100644
index 0000000000..ee10c559f4
--- /dev/null
+++ b/patches/server/0059-Player-Tab-List-and-Title-APIs.patch
@@ -0,0 +1,109 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Techcable <[email protected]>
+Date: Thu, 3 Mar 2016 02:32:10 -0600
+Subject: [PATCH] Player Tab List and Title APIs
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index aa5755bd263c7682e41eeacbde28934b40796cf5..8fd5aaa16b0a6d34f54424512335020bd2e7ecb8 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -393,6 +393,98 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ }
+ }
+
++ // Paper start
++ @Override
++ public void setPlayerListHeaderFooter(BaseComponent[] header, BaseComponent[] footer) {
++ if (header != null) {
++ String headerJson = net.md_5.bungee.chat.ComponentSerializer.toString(header);
++ playerListHeader = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(headerJson);
++ } else {
++ playerListHeader = null;
++ }
++
++ if (footer != null) {
++ String footerJson = net.md_5.bungee.chat.ComponentSerializer.toString(footer);
++ playerListFooter = net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(footerJson);
++ } else {
++ playerListFooter = null;
++ }
++
++ updatePlayerListHeaderFooter();
++ }
++
++ @Override
++ public void setPlayerListHeaderFooter(BaseComponent header, BaseComponent footer) {
++ this.setPlayerListHeaderFooter(header == null ? null : new BaseComponent[]{header},
++ footer == null ? null : new BaseComponent[]{footer});
++ }
++
++
++ @Override
++ public void setTitleTimes(int fadeInTicks, int stayTicks, int fadeOutTicks) {
++ getHandle().connection.send(new ClientboundSetTitlesAnimationPacket(fadeInTicks, stayTicks, fadeOutTicks));
++ }
++
++ @Override
++ public void setSubtitle(BaseComponent[] subtitle) {
++ final ClientboundSetSubtitleTextPacket packet = new ClientboundSetSubtitleTextPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(subtitle)));
++ getHandle().connection.send(packet);
++ }
++
++ @Override
++ public void setSubtitle(BaseComponent subtitle) {
++ setSubtitle(new BaseComponent[]{subtitle});
++ }
++
++ @Override
++ public void showTitle(BaseComponent[] title) {
++ final ClientboundSetTitleTextPacket packet = new ClientboundSetTitleTextPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(title)));
++ getHandle().connection.send(packet);
++ }
++
++ @Override
++ public void showTitle(BaseComponent title) {
++ showTitle(new BaseComponent[]{title});
++ }
++
++ @Override
++ public void showTitle(BaseComponent[] title, BaseComponent[] subtitle, int fadeInTicks, int stayTicks, int fadeOutTicks) {
++ setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks);
++ setSubtitle(subtitle);
++ showTitle(title);
++ }
++
++ @Override
++ public void showTitle(BaseComponent title, BaseComponent subtitle, int fadeInTicks, int stayTicks, int fadeOutTicks) {
++ setTitleTimes(fadeInTicks, stayTicks, fadeOutTicks);
++ setSubtitle(subtitle);
++ showTitle(title);
++ }
++
++ @Override
++ public void sendTitle(com.destroystokyo.paper.Title title) {
++ Preconditions.checkNotNull(title, "Title is null");
++ setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut());
++ setSubtitle(title.getSubtitle() == null ? new BaseComponent[0] : title.getSubtitle());
++ showTitle(title.getTitle());
++ }
++
++ @Override
++ public void updateTitle(com.destroystokyo.paper.Title title) {
++ Preconditions.checkNotNull(title, "Title is null");
++ setTitleTimes(title.getFadeIn(), title.getStay(), title.getFadeOut());
++ if (title.getSubtitle() != null) {
++ setSubtitle(title.getSubtitle());
++ }
++ showTitle(title.getTitle());
++ }
++
++ @Override
++ public void hideTitle() {
++ getHandle().connection.send(new ClientboundClearTitlesPacket(false));
++ }
++ // Paper end
++
+ @Override
+ public String getDisplayName() {
+ if(true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper
diff --git a/patches/server/0060-Add-configurable-portal-search-radius.patch b/patches/server/0060-Add-configurable-portal-search-radius.patch
new file mode 100644
index 0000000000..43fb1eff61
--- /dev/null
+++ b/patches/server/0060-Add-configurable-portal-search-radius.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Joseph Hirschfeld <[email protected]>
+Date: Thu, 3 Mar 2016 02:46:17 -0600
+Subject: [PATCH] Add configurable portal search radius
+
+
+diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java
+index 6bdee85d11b86e618172089c8f9390b6071511ef..a8cb0d5019d06de64b2cc30e6830bbd6ad43a155 100644
+--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java
+@@ -145,8 +145,14 @@ public class NetherPortalBlock extends Block implements Portal {
+ WorldBorder worldborder = worldserver1.getWorldBorder();
+ double d0 = DimensionType.getTeleportationScale(world.dimensionType(), worldserver1.dimensionType());
+ BlockPos blockposition1 = worldborder.clampToBounds(entity.getX() * d0, entity.getY(), entity.getZ() * d0);
++ // Paper start - Configurable portal search radius
++ int portalSearchRadius = worldserver1.paperConfig().environment.portalSearchRadius;
++ if (entity.level().paperConfig().environment.portalSearchVanillaDimensionScaling && flag) { // flag = is going to nether
++ portalSearchRadius = (int) (portalSearchRadius / worldserver1.dimensionType().coordinateScale());
++ }
++ // Paper end - Configurable portal search radius
+ // CraftBukkit start
+- CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(blockposition1, worldserver1.getWorld()), PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, flag ? 16 : 128, 16);
++ CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(blockposition1, worldserver1.getWorld()), PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, portalSearchRadius, worldserver1.paperConfig().environment.portalCreateRadius); // Paper - use custom portal search radius
+ if (event == null) {
+ return null;
+ }
+diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+index 355f1ce10f9564c7c0be505a5af849e0428fec17..eb409fb5e673d2a343813946cc59cb5da2328eec 100644
+--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+@@ -42,6 +42,7 @@ public class PortalForcer {
+ this.level = world;
+ }
+
++ @io.papermc.paper.annotation.DoNotUse // Paper
+ public Optional<BlockPos> findClosestPortalPosition(BlockPos pos, boolean destIsNether, WorldBorder worldBorder) {
+ // CraftBukkit start
+ return this.findClosestPortalPosition(pos, worldBorder, destIsNether ? 16 : 128); // Search Radius
diff --git a/patches/server/0061-Add-velocity-warnings.patch b/patches/server/0061-Add-velocity-warnings.patch
new file mode 100644
index 0000000000..3e32e12869
--- /dev/null
+++ b/patches/server/0061-Add-velocity-warnings.patch
@@ -0,0 +1,85 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Joseph Hirschfeld <[email protected]>
+Date: Thu, 3 Mar 2016 02:48:12 -0600
+Subject: [PATCH] Add velocity warnings
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index e8528943c4c2f5c03a6a83d68133bace86b7901a..3422b2978eb69c537c19abacbb10ce8a5423756a 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -305,6 +305,7 @@ public final class CraftServer implements Server {
+ private final List<CraftPlayer> playerView;
+ public int reloadCount;
+ public Set<String> activeCompatibilities = Collections.emptySet();
++ public static Exception excessiveVelEx; // Paper - Velocity warnings
+
+ static {
+ ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index ea27931d01c1f3c721b2f7ec12d41ea843fa158a..0bec53dc1be4aa997be9f03bc3cde30d22cc8160 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -132,10 +132,40 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ public void setVelocity(Vector velocity) {
+ Preconditions.checkArgument(velocity != null, "velocity");
+ velocity.checkFinite();
++ // Paper start - Warn server owners when plugins try to set super high velocities
++ if (!(this instanceof org.bukkit.entity.Projectile || this instanceof org.bukkit.entity.Minecart) && isUnsafeVelocity(velocity)) {
++ CraftServer.excessiveVelEx = new Exception("Excessive velocity set detected: tried to set velocity of entity " + entity.getScoreboardName() + " id #" + getEntityId() + " to (" + velocity.getX() + "," + velocity.getY() + "," + velocity.getZ() + ").");
++ }
++ // Paper end
+ this.entity.setDeltaMovement(CraftVector.toNMS(velocity));
+ this.entity.hurtMarked = true;
+ }
+
++ // Paper start
++ /**
++ * Checks if the given velocity is not necessarily safe in all situations.
++ * This function returning true does not mean the velocity is dangerous or to be avoided, only that it may be
++ * a detriment to performance on the server.
++ *
++ * It is not to be used as a hard rule of any sort.
++ * Paper only uses it to warn server owners in watchdog crashes.
++ *
++ * @param vel incoming velocity to check
++ * @return if the velocity has the potential to be a performance detriment
++ */
++ private static boolean isUnsafeVelocity(Vector vel) {
++ final double x = vel.getX();
++ final double y = vel.getY();
++ final double z = vel.getZ();
++
++ if (x > 4 || x < -4 || y > 4 || y < -4 || z > 4 || z < -4) {
++ return true;
++ }
++
++ return false;
++ }
++ // Paper end
++
+ @Override
+ public double getHeight() {
+ return this.getHandle().getBbHeight();
+diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
+index e086765dec32241bc5a77afcf072c77a40c6d785..35c90aaee30980610d168c01cbe9abfe04331cb8 100644
+--- a/src/main/java/org/spigotmc/WatchdogThread.java
++++ b/src/main/java/org/spigotmc/WatchdogThread.java
+@@ -81,6 +81,17 @@ public class WatchdogThread extends Thread
+ log.log( Level.SEVERE, "near " + net.minecraft.world.level.Level.lastPhysicsProblem );
+ }
+ //
++ // Paper start - Warn in watchdog if an excessive velocity was ever set
++ if (org.bukkit.craftbukkit.CraftServer.excessiveVelEx != null) {
++ log.log(Level.SEVERE, "------------------------------");
++ log.log(Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity");
++ log.log(Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated");
++ log.log(Level.SEVERE, org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getMessage());
++ for (StackTraceElement stack : org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getStackTrace()) {
++ log.log( Level.SEVERE, "\t\t" + stack );
++ }
++ }
++ // Paper end
+ log.log( Level.SEVERE, "------------------------------" );
+ log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
diff --git a/patches/server/0062-Add-exception-reporting-event.patch b/patches/server/0062-Add-exception-reporting-event.patch
new file mode 100644
index 0000000000..6337d72deb
--- /dev/null
+++ b/patches/server/0062-Add-exception-reporting-event.patch
@@ -0,0 +1,207 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Joseph Hirschfeld <[email protected]>
+Date: Thu, 3 Mar 2016 03:15:41 -0600
+Subject: [PATCH] Add exception reporting event
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/ServerSchedulerReportingWrapper.java b/src/main/java/com/destroystokyo/paper/ServerSchedulerReportingWrapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f699ce18ca044f813e194ef2786b7ea853ea86e7
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/ServerSchedulerReportingWrapper.java
+@@ -0,0 +1,38 @@
++package com.destroystokyo.paper;
++
++import com.google.common.base.Preconditions;
++import org.bukkit.craftbukkit.scheduler.CraftTask;
++import com.destroystokyo.paper.event.server.ServerExceptionEvent;
++import com.destroystokyo.paper.exception.ServerSchedulerException;
++
++/**
++ * Reporting wrapper to catch exceptions not natively
++ */
++public class ServerSchedulerReportingWrapper implements Runnable {
++
++ private final CraftTask internalTask;
++
++ public ServerSchedulerReportingWrapper(CraftTask internalTask) {
++ this.internalTask = Preconditions.checkNotNull(internalTask, "internalTask");
++ }
++
++ @Override
++ public void run() {
++ try {
++ internalTask.run();
++ } catch (RuntimeException e) {
++ internalTask.getOwner().getServer().getPluginManager().callEvent(
++ new ServerExceptionEvent(new ServerSchedulerException(e, internalTask))
++ );
++ throw e;
++ } catch (Throwable t) {
++ internalTask.getOwner().getServer().getPluginManager().callEvent(
++ new ServerExceptionEvent(new ServerSchedulerException(t, internalTask))
++ ); //Do not rethrow, since it is not permitted with Runnable#run
++ }
++ }
++
++ public CraftTask getInternalTask() {
++ return internalTask;
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+index 68551947f5b7d3471f15bd74ccd86519ab34c1c1..a0b0614ac7d2009db5c6c10ab4a5f09dd447c635 100644
+--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
++++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+@@ -356,7 +356,11 @@ public class OldUsersConverter {
+ try {
+ root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap());
+ } catch (Exception exception) {
+- io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
++ // Paper start
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
++ exception.printStackTrace();
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception);
++ // Paper end
+ }
+
+ if (root != null) {
+@@ -369,7 +373,11 @@ public class OldUsersConverter {
+ try {
+ NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
+ } catch (Exception exception) {
+- io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Paper
++ // Paper start
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
++ exception.printStackTrace();
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception);
++ // Paper end
+ }
+ }
+ // CraftBukkit end
+diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
+index a5b18a04f482d05d3ca74918a580499b21c2fc3c..bd3f71c3eaa33258ff56062ea3a2099cef310b7a 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
++++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
+@@ -117,6 +117,7 @@ public class VillageSiege implements CustomSpawner {
+ entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), EntitySpawnReason.EVENT, (SpawnGroupData) null);
+ } catch (Exception exception) {
+ VillageSiege.LOGGER.warn("Failed to create zombie for village siege at {}", vec3d, exception);
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
+ return;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index d8621149add06021d594415e079771b6fd57cc3f..11c5191ec6a53cb42f8a75e249fbce1058f2b58e 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -729,6 +729,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ // Paper start - Prevent block entity and entity crashes
+ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
+ MinecraftServer.LOGGER.error(msg, throwable);
++ getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
+ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
+ // Paper end - Prevent block entity and entity crashes
+ }
+diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+index 9389fd53f2bff0a9ca389694b312dc6da58befaf..da0ddb8285e157be0cc7b940a9590be5e3061e3d 100644
+--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+@@ -294,6 +294,7 @@ public final class NaturalSpawner {
+ NaturalSpawner.LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(type));
+ } catch (Exception exception) {
+ NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
+ }
+
+ return null;
+@@ -382,6 +383,7 @@ public final class NaturalSpawner {
+ entity = biomesettingsmobs_c.type.create(world.getLevel(), EntitySpawnReason.NATURAL);
+ } catch (Exception exception) {
+ NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
+ continue;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index daa2b3f67765bc340404747b3cffea5604830515..95da29b973d43b59d9c4d0c83068dc74b59c9c8b 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -501,8 +501,13 @@ public class LevelChunk extends ChunkAccess {
+ BlockState iblockdata = this.getBlockState(blockposition);
+
+ if (!iblockdata.hasBlockEntity()) {
+- LevelChunk.LOGGER.warn("Trying to set block entity {} at position {}, but state {} does not allow it", new Object[]{blockEntity, blockposition, iblockdata});
+- new Exception().printStackTrace(); // CraftBukkit
++ // Paper start - ServerExceptionEvent
++ com.destroystokyo.paper.exception.ServerInternalException e = new com.destroystokyo.paper.exception.ServerInternalException(
++ "Trying to set block entity %s at position %s, but state %s does not allow it".formatted(blockEntity, blockposition, iblockdata)
++ );
++ e.printStackTrace();
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(e);
++ // Paper end - ServerExceptionEvent
+ } else {
+ BlockState iblockdata1 = blockEntity.getBlockState();
+
+@@ -991,6 +996,7 @@ public class LevelChunk extends ChunkAccess {
+ // Paper start - Prevent block entity and entity crashes
+ final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
+ net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable);
++ net.minecraft.world.level.chunk.LevelChunk.this.level.getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
+ LevelChunk.this.removeBlockEntity(this.getPos());
+ // Paper end - Prevent block entity and entity crashes
+ // Spigot start
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+index 15f273aa592828719de6e092d79a407dc8652dfe..b24e8255ab18eb5b2e4968aa62aa3d72ef33f0eb 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -296,6 +296,7 @@ public class RegionFile implements AutoCloseable {
+ return true;
+ }
+ } catch (IOException ioexception) {
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(ioexception); // Paper - ServerExceptionEvent
+ return false;
+ }
+ }
+@@ -377,6 +378,7 @@ public class RegionFile implements AutoCloseable {
+ ((java.nio.Buffer) buf).position(5); // CraftBukkit - decompile error
+ filechannel.write(buf);
+ } catch (Throwable throwable) {
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(throwable); // Paper - ServerExceptionEvent
+ if (filechannel != null) {
+ try {
+ filechannel.close();
+diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+index 0385aa1e5cced22bbafdabca1b63599db1f5d3f6..152c816efbd633210ff308ef56f5d0cda6edb1b5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+@@ -415,20 +415,25 @@ public class CraftScheduler implements BukkitScheduler {
+ try {
+ task.run();
+ } catch (final Throwable throwable) {
++ // Paper start
++ final String logMessage = String.format(
++ "Task #%s for %s generated an exception",
++ task.getTaskId(),
++ task.getOwner().getDescription().getFullName());
+ task.getOwner().getLogger().log(
+ Level.WARNING,
+- String.format(
+- "Task #%s for %s generated an exception",
+- task.getTaskId(),
+- task.getOwner().getDescription().getFullName()),
++ logMessage,
+ throwable);
++ org.bukkit.Bukkit.getServer().getPluginManager().callEvent(
++ new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerSchedulerException(logMessage, throwable, task)));
++ // Paper end
+ } finally {
+ this.currentTask = null;
+ }
+ this.parsePending();
+ } else {
+ this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(this.currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass()));
+- this.executor.execute(task);
++ this.executor.execute(new com.destroystokyo.paper.ServerSchedulerReportingWrapper(task)); // Paper
+ // We don't need to parse pending
+ // (async tasks must live with race-conditions if they attempt to cancel between these few lines of code)
+ }
diff --git a/patches/server/0063-Disable-Scoreboards-for-non-players-by-default.patch b/patches/server/0063-Disable-Scoreboards-for-non-players-by-default.patch
new file mode 100644
index 0000000000..f1fe0283de
--- /dev/null
+++ b/patches/server/0063-Disable-Scoreboards-for-non-players-by-default.patch
@@ -0,0 +1,36 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Tue, 8 Mar 2016 23:25:45 -0500
+Subject: [PATCH] Disable Scoreboards for non players by default
+
+Entities collision is checking for scoreboards setting.
+This is very heavy to do map lookups for every collision to check
+this setting.
+
+So avoid looking up scoreboards and short circuit to the "not on a team"
+logic which is most likely to be true.
+
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index 61f7461150c7354a641b86ce42a2b41460d3a45d..ad16a318033bb1f24c811ad6eebe2f76eb987408 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -3088,6 +3088,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+
+ @Nullable
+ public PlayerTeam getTeam() {
++ if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default
+ return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName());
+ }
+
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index 25d2aa773f67946bf18312975c5b25f93015c39c..b8d70367d4d0a7a384b7ac723a02739fb5d741e5 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -871,6 +871,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ String s = nbt.getString("Team");
+ Scoreboard scoreboard = this.level().getScoreboard();
+ PlayerTeam scoreboardteam = scoreboard.getPlayerTeam(s);
++ if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper - Perf: Disable Scoreboards for non players by default
+ boolean flag = scoreboardteam != null && scoreboard.addPlayerToTeam(this.getStringUUID(), scoreboardteam);
+
+ if (!flag) {
diff --git a/patches/server/0064-Add-methods-for-working-with-arrows-stuck-in-living-.patch b/patches/server/0064-Add-methods-for-working-with-arrows-stuck-in-living-.patch
new file mode 100644
index 0000000000..4b2ba339c3
--- /dev/null
+++ b/patches/server/0064-Add-methods-for-working-with-arrows-stuck-in-living-.patch
@@ -0,0 +1,60 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: mrapple <[email protected]>
+Date: Sun, 25 Nov 2012 13:43:39 -0600
+Subject: [PATCH] Add methods for working with arrows stuck in living entities
+
+Upstream added methods for this, original methods are now
+deprecated
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+index b34829d780d9f695b8109f4baed7cc251f2b6fdc..7d05d87e3c8ebbd65b15dcf09324e2e4f95959e5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java
+@@ -289,10 +289,29 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
+ }
+
+ @Override
+- public void setArrowsInBody(int count) {
++ public void setArrowsInBody(final int count, final boolean fireEvent) { // Paper
+ Preconditions.checkArgument(count >= 0, "New arrow amount must be >= 0");
++ if (!fireEvent) { // Paper
+ this.getHandle().getEntityData().set(net.minecraft.world.entity.LivingEntity.DATA_ARROW_COUNT_ID, count);
++ // Paper start
++ } else {
++ this.getHandle().setArrowCount(count);
++ }
++ // Paper end
++ }
++
++ // Paper start - Add methods for working with arrows stuck in living entities
++ @Override
++ public void setNextArrowRemoval(final int ticks) {
++ Preconditions.checkArgument(ticks >= 0, "New amount of ticks before next arrow removal must be >= 0");
++ this.getHandle().removeArrowTime = ticks;
++ }
++
++ @Override
++ public int getNextArrowRemoval() {
++ return this.getHandle().removeArrowTime;
+ }
++ // Paper end - Add methods for working with arrows stuck in living entities
+
+ @Override
+ public boolean isInvulnerable() {
+@@ -833,4 +852,16 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
+ this.getHandle().persistentInvisibility = invisible;
+ this.getHandle().setSharedFlag(5, invisible);
+ }
++
++ // Paper start
++ @Override
++ public int getArrowsStuck() {
++ return this.getHandle().getArrowCount();
++ }
++
++ @Override
++ public void setArrowsStuck(final int arrows) {
++ this.getHandle().setArrowCount(arrows);
++ }
++ // Paper end
+ }
diff --git a/patches/server/0065-Complete-resource-pack-API.patch b/patches/server/0065-Complete-resource-pack-API.patch
new file mode 100644
index 0000000000..9504d643e4
--- /dev/null
+++ b/patches/server/0065-Complete-resource-pack-API.patch
@@ -0,0 +1,49 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jedediah Smith <[email protected]>
+Date: Sat, 4 Apr 2015 23:17:52 -0400
+Subject: [PATCH] Complete resource pack API
+
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+index 99f89854e43ed6742dc9ac1624fa7140b4594b3b..d4527831f66bf1c55e6273c7f8923d6efbbf100f 100644
+--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+@@ -214,7 +214,11 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), this.getCraftPlayer());
+ }
+ // Paper end
+- this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()])); // CraftBukkit
++ // Paper start - store last pack status
++ PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
++ player.getBukkitEntity().resourcePackStatus = packStatus;
++ this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
++ // Paper end - store last pack status
+
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 8fd5aaa16b0a6d34f54424512335020bd2e7ecb8..dd3072717a28ae74914e0806f1874994f9ae5f5b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -213,6 +213,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ private double healthScale = 20;
+ private CraftWorldBorder clientWorldBorder = null;
+ private BorderChangeListener clientWorldBorderListener = this.createWorldBorderListener();
++ public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; // Paper - more resource pack API
+
+ public CraftPlayer(CraftServer server, ServerPlayer entity) {
+ super(server, entity);
+@@ -2121,6 +2122,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ }
+ // Paper end - adventure
+
++ // Paper start - more resource pack API
++ @Override
++ public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status getResourcePackStatus() {
++ return this.resourcePackStatus;
++ }
++ // Paper end - more resource pack API
++
+ @Override
+ public void removeResourcePack(UUID id) {
+ Preconditions.checkArgument(id != null, "Resource pack id cannot be null");
diff --git a/patches/server/0066-Default-loading-permissions.yml-before-plugins.patch b/patches/server/0066-Default-loading-permissions.yml-before-plugins.patch
new file mode 100644
index 0000000000..45510139a9
--- /dev/null
+++ b/patches/server/0066-Default-loading-permissions.yml-before-plugins.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Fri, 18 Mar 2016 13:17:38 -0400
+Subject: [PATCH] Default loading permissions.yml before plugins
+
+Under previous behavior, plugins were not able to check if a player had a permission
+if it was defined in permissions.yml. there is no clean way for a plugin to fix that either.
+
+This will change the order so that by default, permissions.yml loads BEFORE plugins instead of after.
+
+This gives plugins expected permission checks.
+
+It also helps improve the expected logic, as servers should set the initial defaults, and then let plugins
+modify that. Under the previous logic, plugins were unable (cleanly) override permissions.yml.
+
+A config option has been added for those who depend on the previous behavior, but I don't expect that.
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 3422b2978eb69c537c19abacbb10ce8a5423756a..248de665ebf0e1ddfb10616f6213fa344cd88d40 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -490,6 +490,7 @@ public final class CraftServer implements Server {
+ if (type == PluginLoadOrder.STARTUP) {
+ this.helpMap.clear();
+ this.helpMap.initializeGeneralTopics();
++ if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions(); // Paper
+ }
+
+ Plugin[] plugins = this.pluginManager.getPlugins();
+@@ -509,7 +510,7 @@ public final class CraftServer implements Server {
+ this.commandMap.registerServerAliases();
+ DefaultPermissions.registerCorePermissions();
+ CraftDefaultPermissions.registerCorePermissions();
+- this.loadCustomPermissions();
++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) this.loadCustomPermissions(); // Paper
+ this.helpMap.initializeCommands();
+ this.syncCommands();
+ }
diff --git a/patches/server/0067-Allow-Reloading-of-Custom-Permissions.patch b/patches/server/0067-Allow-Reloading-of-Custom-Permissions.patch
new file mode 100644
index 0000000000..9b62fef16d
--- /dev/null
+++ b/patches/server/0067-Allow-Reloading-of-Custom-Permissions.patch
@@ -0,0 +1,35 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: William <[email protected]>
+Date: Fri, 18 Mar 2016 03:30:17 -0400
+Subject: [PATCH] Allow Reloading of Custom Permissions
+
+https://github.com/PaperMC/Paper/issues/49
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 248de665ebf0e1ddfb10616f6213fa344cd88d40..a8ee4d5b932e42367df9e74dacd0e3f1fdbe7d11 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -2818,5 +2818,23 @@ public final class CraftServer implements Server {
+ }
+ return this.adventure$audiences;
+ }
++
++ @Override
++ public void reloadPermissions() {
++ pluginManager.clearPermissions();
++ if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
++ for (Plugin plugin : pluginManager.getPlugins()) {
++ for (Permission perm : plugin.getDescription().getPermissions()) {
++ try {
++ pluginManager.addPermission(perm);
++ } catch (IllegalArgumentException ex) {
++ getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
++ }
++ }
++ }
++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
++ DefaultPermissions.registerCorePermissions();
++ CraftDefaultPermissions.registerCorePermissions();
++ }
+ // Paper end
+ }
diff --git a/patches/server/0068-Remove-Metadata-on-reload.patch b/patches/server/0068-Remove-Metadata-on-reload.patch
new file mode 100644
index 0000000000..61752107c6
--- /dev/null
+++ b/patches/server/0068-Remove-Metadata-on-reload.patch
@@ -0,0 +1,29 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Fri, 18 Mar 2016 13:50:14 -0400
+Subject: [PATCH] Remove Metadata on reload
+
+Metadata is not meant to persist reload as things break badly with non primitive types
+This will remove metadata on reload so it does not crash everything if a plugin uses it.
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index a8ee4d5b932e42367df9e74dacd0e3f1fdbe7d11..c6a0765e6cfbe90444b91a19039483070600df6d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -1005,8 +1005,16 @@ public final class CraftServer implements Server {
+ world.spigotConfig.init(); // Spigot
+ }
+
++ Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper
+ this.pluginManager.clearPlugins();
+ this.commandMap.clearCommands();
++ // Paper start
++ for (Plugin plugin : pluginClone) {
++ entityMetadata.removeAll(plugin);
++ worldMetadata.removeAll(plugin);
++ playerMetadata.removeAll(plugin);
++ }
++ // Paper end
+ this.reloadData();
+ org.spigotmc.SpigotConfig.registerCommands(); // Spigot
+ io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper
diff --git a/patches/server/0069-Handle-Item-Meta-Inconsistencies.patch b/patches/server/0069-Handle-Item-Meta-Inconsistencies.patch
new file mode 100644
index 0000000000..bd6c98fc50
--- /dev/null
+++ b/patches/server/0069-Handle-Item-Meta-Inconsistencies.patch
@@ -0,0 +1,303 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 28 May 2015 23:00:19 -0400
+Subject: [PATCH] Handle Item Meta Inconsistencies
+
+First, Enchantment order would blow away seeing 2 items as the same,
+however the Client forces enchantment list in a certain order, as well
+as does the /enchant command. Anvils can insert it into forced order,
+causing 2 same items to be considered different.
+
+This change makes unhandled NBT Tags and Enchantments use a sorted tree map,
+so they will always be in a consistent order.
+
+Additionally, the old enchantment API was never updated when ItemMeta
+was added, resulting in 2 different ways to modify an items enchantments.
+
+For consistency, the old API methods now forward to use the
+ItemMeta API equivalents, and should deprecate the old API's.
+
+diff --git a/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java b/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java
+index bc6c2c24174181315c5622ba0dbe578b4dbcc627..cfc6a657cae92c68868a76c1b7b1febe2a16e9f4 100644
+--- a/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java
++++ b/src/main/java/net/minecraft/world/item/enchantment/ItemEnchantments.java
+@@ -26,12 +26,25 @@ import net.minecraft.tags.TagKey;
+ import net.minecraft.world.item.Item;
+ import net.minecraft.world.item.TooltipFlag;
+ import net.minecraft.world.item.component.TooltipProvider;
++// Paper start
++import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap;
++// Paper end
+
+ public class ItemEnchantments implements TooltipProvider {
+- public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntOpenHashMap<>(), true);
++ // Paper start
++ private static final java.util.Comparator<Holder<Enchantment>> ENCHANTMENT_ORDER = java.util.Comparator.comparing(Holder::getRegisteredName);
++ public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), true);
++ // Paper end
+ private static final Codec<Integer> LEVEL_CODEC = Codec.intRange(1, 255);
+- private static final Codec<Object2IntOpenHashMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(Enchantment.CODEC, LEVEL_CODEC)
+- .xmap(Object2IntOpenHashMap::new, Function.identity());
++ private static final Codec<Object2IntAVLTreeMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(
++ Enchantment.CODEC, LEVEL_CODEC
++ )// Paper start - sort enchantments
++ .xmap(m -> {
++ final Object2IntAVLTreeMap<Holder<Enchantment>> map = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER);
++ map.putAll(m);
++ return map;
++ }, Function.identity());
++ // Paper end - sort enchantments
+ private static final Codec<ItemEnchantments> FULL_CODEC = RecordCodecBuilder.create(
+ instance -> instance.group(
+ LEVELS_CODEC.fieldOf("levels").forGetter(component -> component.enchantments),
+@@ -41,16 +54,16 @@ public class ItemEnchantments implements TooltipProvider {
+ );
+ public static final Codec<ItemEnchantments> CODEC = Codec.withAlternative(FULL_CODEC, LEVELS_CODEC, map -> new ItemEnchantments(map, true));
+ public static final StreamCodec<RegistryFriendlyByteBuf, ItemEnchantments> STREAM_CODEC = StreamCodec.composite(
+- ByteBufCodecs.map(Object2IntOpenHashMap::new, Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
++ ByteBufCodecs.map((v) -> new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
+ component -> component.enchantments,
+ ByteBufCodecs.BOOL,
+ component -> component.showInTooltip,
+ ItemEnchantments::new
+ );
+- final Object2IntOpenHashMap<Holder<Enchantment>> enchantments;
++ final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments; // Paper
+ public final boolean showInTooltip;
+
+- ItemEnchantments(Object2IntOpenHashMap<Holder<Enchantment>> enchantments, boolean showInTooltip) {
++ ItemEnchantments(Object2IntAVLTreeMap<Holder<Enchantment>> enchantments, boolean showInTooltip) { // Paper
+ this.enchantments = enchantments;
+ this.showInTooltip = showInTooltip;
+
+@@ -139,7 +152,7 @@ public class ItemEnchantments implements TooltipProvider {
+ }
+
+ public static class Mutable {
+- private final Object2IntOpenHashMap<Holder<Enchantment>> enchantments = new Object2IntOpenHashMap<>();
++ private final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER); // Paper
+ public boolean showInTooltip;
+
+ public Mutable(ItemEnchantments enchantmentsComponent) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+index 101eea3452c9e387e770b716543c3a4f17b9a737..aea09533fada5bd3d42e2cc147921167a5e7c1a5 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -229,16 +229,13 @@ public final class CraftItemStack extends ItemStack {
+ public void addUnsafeEnchantment(Enchantment ench, int level) {
+ Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
+
+- if (!CraftItemStack.makeTag(this.handle)) {
+- return;
+- }
+- ItemEnchantments list = CraftItemStack.getEnchantmentList(this.handle);
+- if (list == null) {
+- list = ItemEnchantments.EMPTY;
++ // Paper start - Replace whole method
++ final ItemMeta itemMeta = this.getItemMeta();
++ if (itemMeta != null) {
++ itemMeta.addEnchant(ench, level, true);
++ this.setItemMeta(itemMeta);
+ }
+- ItemEnchantments.Mutable listCopy = new ItemEnchantments.Mutable(list);
+- listCopy.set(CraftEnchantment.bukkitToMinecraftHolder(ench), level);
+- this.handle.set(DataComponents.ENCHANTMENTS, listCopy.toImmutable());
++ // Paper end
+ }
+
+ static boolean makeTag(net.minecraft.world.item.ItemStack item) {
+@@ -267,24 +264,15 @@ public final class CraftItemStack extends ItemStack {
+ public int removeEnchantment(Enchantment ench) {
+ Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
+
+- ItemEnchantments list = CraftItemStack.getEnchantmentList(this.handle);
+- if (list == null) {
+- return 0;
+- }
+- int level = this.getEnchantmentLevel(ench);
+- if (level <= 0) {
+- return 0;
+- }
+- int size = list.size();
+-
+- if (size == 1) {
+- this.handle.remove(DataComponents.ENCHANTMENTS);
+- return level;
++ // Paper start - replace entire method
++ int level = getEnchantmentLevel(ench);
++ if (level > 0) {
++ final ItemMeta itemMeta = this.getItemMeta();
++ if (itemMeta == null) return 0;
++ itemMeta.removeEnchant(ench);
++ this.setItemMeta(itemMeta);
+ }
+-
+- ItemEnchantments.Mutable listCopy = new ItemEnchantments.Mutable(list);
+- listCopy.set(CraftEnchantment.bukkitToMinecraftHolder(ench), -1); // Negative to remove
+- this.handle.set(DataComponents.ENCHANTMENTS, listCopy.toImmutable());
++ // Paper end
+
+ return level;
+ }
+@@ -296,7 +284,7 @@ public final class CraftItemStack extends ItemStack {
+
+ @Override
+ public Map<Enchantment, Integer> getEnchantments() {
+- return CraftItemStack.getEnchantments(this.handle);
++ return this.hasItemMeta() ? this.getItemMeta().getEnchants() : ImmutableMap.<Enchantment, Integer>of(); // Paper - use Item Meta
+ }
+
+ static Map<Enchantment, Integer> getEnchantments(net.minecraft.world.item.ItemStack item) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+index 784db8fa1b9ef99755440c6446248b802445da67..3c44f5509e63dd673f0b8e701720984b78b9b7c4 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+@@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableList;
+ import com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.ImmutableMultimap;
+ import com.google.common.collect.LinkedHashMultimap;
++import com.google.common.collect.ImmutableSortedMap; // Paper
+ import com.google.common.collect.Lists;
+ import com.google.common.collect.Multimap;
+ import com.google.common.collect.SetMultimap;
+@@ -23,6 +24,7 @@ import java.util.Arrays;
+ import java.util.Base64;
+ import java.util.Collection;
+ import java.util.Collections;
++import java.util.Comparator; // Paper
+ import java.util.EnumSet;
+ import java.util.HashMap;
+ import java.util.Iterator;
+@@ -283,7 +285,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ private CraftCustomModelDataComponent customModelData;
+ private Integer enchantableValue;
+ private Map<String, String> blockData;
+- private Map<Enchantment, Integer> enchantments;
++ private EnchantmentMap enchantments; // Paper
+ private Multimap<Attribute, AttributeModifier> attributeModifiers;
+ private int repairCost;
+ private int hideFlag;
+@@ -334,7 +336,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ this.blockData = meta.blockData;
+
+ if (meta.enchantments != null) {
+- this.enchantments = new LinkedHashMap<Enchantment, Integer>(meta.enchantments);
++ this.enchantments = new EnchantmentMap(meta.enchantments); // Paper
+ }
+
+ if (meta.hasAttributeModifiers()) {
+@@ -513,8 +515,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ }
+ }
+
+- static Map<Enchantment, Integer> buildEnchantments(ItemEnchantments tag) {
+- Map<Enchantment, Integer> enchantments = new LinkedHashMap<Enchantment, Integer>(tag.size());
++ static EnchantmentMap buildEnchantments(ItemEnchantments tag) { // Paper
++ EnchantmentMap enchantments = new EnchantmentMap(); // Paper
+
+ tag.entrySet().forEach((entry) -> {
+ Holder<net.minecraft.world.item.enchantment.Enchantment> id = entry.getKey();
+@@ -850,13 +852,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ return modifiers;
+ }
+
+- static Map<Enchantment, Integer> buildEnchantments(Map<String, Object> map, ItemMetaKey key) {
++ static EnchantmentMap buildEnchantments(Map<String, Object> map, ItemMetaKey key) { // Paper
+ Map<?, ?> ench = SerializableMeta.getObject(Map.class, map, key.BUKKIT, true);
+ if (ench == null) {
+ return null;
+ }
+
+- Map<Enchantment, Integer> enchantments = new LinkedHashMap<Enchantment, Integer>(ench.size());
++ EnchantmentMap enchantments = new EnchantmentMap(); // Paper
+ for (Map.Entry<?, ?> entry : ench.entrySet()) {
+ Enchantment enchantment = CraftEnchantment.stringToBukkit(entry.getKey().toString());
+ if ((enchantment != null) && (entry.getValue() instanceof Integer)) {
+@@ -1223,14 +1225,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+
+ @Override
+ public Map<Enchantment, Integer> getEnchants() {
+- return this.hasEnchants() ? ImmutableMap.copyOf(this.enchantments) : ImmutableMap.<Enchantment, Integer>of();
++ return this.hasEnchants() ? ImmutableSortedMap.copyOfSorted(this.enchantments) : ImmutableMap.<Enchantment, Integer>of(); // Paper
+ }
+
+ @Override
+ public boolean addEnchant(Enchantment ench, int level, boolean ignoreRestrictions) {
+ Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
+ if (this.enchantments == null) {
+- this.enchantments = new LinkedHashMap<Enchantment, Integer>(4);
++ this.enchantments = new EnchantmentMap(); // Paper
+ }
+
+ if (ignoreRestrictions || level >= ench.getStartLevel() && level <= ench.getMaxLevel()) {
+@@ -1976,7 +1978,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ clone.enchantableValue = this.enchantableValue;
+ clone.blockData = this.blockData;
+ if (this.enchantments != null) {
+- clone.enchantments = new LinkedHashMap<Enchantment, Integer>(this.enchantments);
++ clone.enchantments = new EnchantmentMap(this.enchantments); // Paper
+ }
+ if (this.hasAttributeModifiers()) {
+ clone.attributeModifiers = LinkedHashMultimap.create(this.attributeModifiers);
+@@ -2372,4 +2374,22 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+
+ return (result != null) ? result : Optional.empty();
+ }
++
++ // Paper start
++ private static class EnchantmentMap extends java.util.TreeMap<org.bukkit.enchantments.Enchantment, Integer> {
++ private EnchantmentMap(Map<Enchantment, Integer> enchantments) {
++ this();
++ putAll(enchantments);
++ }
++
++ private EnchantmentMap() {
++ super(Comparator.comparing(o -> o.getKey().toString()));
++ }
++
++ public EnchantmentMap clone() {
++ return (EnchantmentMap) super.clone();
++ }
++ }
++ // Paper end
++
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
+index e7deba518b8a3df9e642a1bcd7b6642c3dc7583b..26a7c2d37e6a8284ab444a9edad967bdcad1e5a3 100644
+--- a/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
++++ b/src/main/java/org/bukkit/craftbukkit/profile/CraftPlayerProfile.java
+@@ -40,6 +40,17 @@ public final class CraftPlayerProfile implements PlayerProfile {
+ boolean isValidSkullProfile = (gameProfile.getName() != null)
+ || gameProfile.getProperties().containsKey(CraftPlayerTextures.PROPERTY_NAME);
+ Preconditions.checkArgument(isValidSkullProfile, "The skull profile is missing a name or textures!");
++ // Paper start - Validate
++ Preconditions.checkArgument(gameProfile.getName().length() <= 16, "The name of the profile is longer than 16 characters");
++ Preconditions.checkArgument(net.minecraft.util.StringUtil.isValidPlayerName(gameProfile.getName()), "The name of the profile contains invalid characters: %s", gameProfile.getName());
++ final PropertyMap properties = gameProfile.getProperties();
++ Preconditions.checkArgument(properties.size() <= 16, "The profile contains more than 16 properties");
++ for (final Property property : properties.values()) {
++ Preconditions.checkArgument(property.name().length() <= 64, "The name of a property is longer than 64 characters");
++ Preconditions.checkArgument(property.value().length() <= Short.MAX_VALUE, "The value of a property is longer than 32767 characters");
++ Preconditions.checkArgument(property.signature() == null || property.signature().length() <= 1024, "The signature of a property is longer than 1024 characters");
++ }
++ // Paper end - Validate
+ return gameProfile;
+ }
+
+@@ -67,6 +78,8 @@ public final class CraftPlayerProfile implements PlayerProfile {
+ if (applyPreconditions) {
+ Preconditions.checkArgument((uniqueId != null) || !StringUtils.isBlank(name), "uniqueId is null or name is blank");
+ }
++ Preconditions.checkArgument(name == null || name.length() <= 16, "The name of the profile is longer than 16 characters"); // Paper - Validate
++ Preconditions.checkArgument(name == null || net.minecraft.util.StringUtil.isValidPlayerName(name), "The name of the profile contains invalid characters: %s", name); // Paper - Validate
+ this.uniqueId = uniqueId;
+ this.name = name;
+ }
+@@ -114,6 +127,7 @@ public final class CraftPlayerProfile implements PlayerProfile {
+ // Assert: (property == null) || property.getName().equals(propertyName)
+ this.removeProperty(propertyName);
+ if (property != null) {
++ Preconditions.checkArgument(this.properties.size() < 16, "The profile contains more than 16 properties"); // Paper - Validate
+ this.properties.put(property.name(), property);
+ }
+ }
diff --git a/patches/server/0070-Configurable-Non-Player-Arrow-Despawn-Rate.patch b/patches/server/0070-Configurable-Non-Player-Arrow-Despawn-Rate.patch
new file mode 100644
index 0000000000..b42858afe6
--- /dev/null
+++ b/patches/server/0070-Configurable-Non-Player-Arrow-Despawn-Rate.patch
@@ -0,0 +1,20 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Fri, 18 Mar 2016 15:12:22 -0400
+Subject: [PATCH] Configurable Non Player Arrow Despawn Rate
+
+Can set a much shorter despawn rate for arrows that players can not pick up.
+
+diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
+index a83efabc1e2ac5f8af8f8a82fdfc37c8fd7b1232..dd115d514aeb3d124bc99456044f7d176850acab 100644
+--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
+@@ -380,7 +380,7 @@ public abstract class AbstractArrow extends Projectile {
+
+ protected void tickDespawn() {
+ ++this.life;
+- if (this.life >= ((this instanceof ThrownTrident) ? this.level().spigotConfig.tridentDespawnRate : this.level().spigotConfig.arrowDespawnRate)) { // Spigot
++ if (this.life >= (pickup == Pickup.CREATIVE_ONLY ? this.level().paperConfig().entities.spawning.creativeArrowDespawnRate.value() : (pickup == Pickup.DISALLOWED ? this.level().paperConfig().entities.spawning.nonPlayerArrowDespawnRate.value() : ((this instanceof ThrownTrident) ? this.level().spigotConfig.tridentDespawnRate : this.level().spigotConfig.arrowDespawnRate)))) { // Spigot // Paper - Configurable non-player arrow despawn rate; TODO: Extract this to init?
+ this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+ }
+
diff --git a/patches/server/0071-Add-World-Util-Methods.patch b/patches/server/0071-Add-World-Util-Methods.patch
new file mode 100644
index 0000000000..86fb726c99
--- /dev/null
+++ b/patches/server/0071-Add-World-Util-Methods.patch
@@ -0,0 +1,34 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Fri, 18 Mar 2016 20:16:03 -0400
+Subject: [PATCH] Add World Util Methods
+
+Methods that can be used for other patches to help improve logic.
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 11c5191ec6a53cb42f8a75e249fbce1058f2b58e..36ed95a432429d7fe977684d26818afdb49c36dc 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -340,6 +340,22 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+
++ public final boolean isLoadedAndInBounds(BlockPos blockposition) { // Paper - final for inline
++ return getWorldBorder().isWithinBounds(blockposition) && getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) != null;
++ }
++
++ public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { // Overridden in WorldServer for ABI compat which has final
++ return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(x, z);
++ }
++ public final @Nullable LevelChunk getChunkIfLoaded(BlockPos blockposition) {
++ return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ }
++
++ // reduces need to do isLoaded before getType
++ public final @Nullable BlockState getBlockStateIfLoadedAndInBounds(BlockPos blockposition) {
++ return getWorldBorder().isWithinBounds(blockposition) ? getBlockStateIfLoaded(blockposition) : null;
++ }
++
+ @Override
+ public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
+ // Paper end
diff --git a/patches/server/0072-Custom-replacement-for-eaten-items.patch b/patches/server/0072-Custom-replacement-for-eaten-items.patch
new file mode 100644
index 0000000000..f49bdb7f65
--- /dev/null
+++ b/patches/server/0072-Custom-replacement-for-eaten-items.patch
@@ -0,0 +1,48 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jedediah Smith <[email protected]>
+Date: Sun, 21 Jun 2015 15:07:20 -0400
+Subject: [PATCH] Custom replacement for eaten items
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index b8d70367d4d0a7a384b7ac723a02739fb5d741e5..58d61cbbca0011b8d8e86dce818de6df4c9d1c83 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -3990,10 +3990,11 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ if (!this.useItem.isEmpty() && this.isUsingItem()) {
+ // CraftBukkit start - fire PlayerItemConsumeEvent
+ ItemStack itemstack;
++ PlayerItemConsumeEvent event = null; // Paper
+ if (this instanceof ServerPlayer entityPlayer) {
+ org.bukkit.inventory.ItemStack craftItem = CraftItemStack.asBukkitCopy(this.useItem);
+ org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(enumhand);
+- PlayerItemConsumeEvent event = new PlayerItemConsumeEvent((Player) this.getBukkitEntity(), craftItem, hand);
++ event = new PlayerItemConsumeEvent((Player) this.getBukkitEntity(), craftItem, hand); // Paper
+ this.level().getCraftServer().getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+@@ -4011,6 +4012,12 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ } else {
+ itemstack = this.useItem.finishUsingItem(this.level(), this);
+ }
++ // Paper start - save the default replacement item and change it if necessary
++ final ItemStack defaultReplacement = itemstack;
++ if (event != null && event.getReplacement() != null) {
++ itemstack = CraftItemStack.asNMSCopy(event.getReplacement());
++ }
++ // Paper end
+ // CraftBukkit end
+
+ if (itemstack != this.useItem) {
+@@ -4018,6 +4025,11 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ this.stopUsingItem();
++ // Paper start - if the replacement is anything but the default, update the client inventory
++ if (this instanceof ServerPlayer && !com.google.common.base.Objects.equal(defaultReplacement, itemstack)) {
++ ((ServerPlayer) this).getBukkitEntity().updateInventory();
++ }
++ // Paper end
+ }
+
+ }
diff --git a/patches/server/0073-handle-NaN-health-absorb-values-and-repair-bad-data.patch b/patches/server/0073-handle-NaN-health-absorb-values-and-repair-bad-data.patch
new file mode 100644
index 0000000000..00e466a32b
--- /dev/null
+++ b/patches/server/0073-handle-NaN-health-absorb-values-and-repair-bad-data.patch
@@ -0,0 +1,57 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sun, 27 Sep 2015 01:18:02 -0400
+Subject: [PATCH] handle NaN health/absorb values and repair bad data
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index 58d61cbbca0011b8d8e86dce818de6df4c9d1c83..de88eec263fd327c3d72e69bcd1c32343379d33e 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -831,7 +831,13 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ @Override
+ public void readAdditionalSaveData(CompoundTag nbt) {
+- this.internalSetAbsorptionAmount(nbt.getFloat("AbsorptionAmount"));
++ // Paper start - Check for NaN
++ float absorptionAmount = nbt.getFloat("AbsorptionAmount");
++ if (Float.isNaN(absorptionAmount)) {
++ absorptionAmount = 0;
++ }
++ this.internalSetAbsorptionAmount(absorptionAmount);
++ // Paper end - Check for NaN
+ if (nbt.contains("attributes", 9) && this.level() != null && !this.level().isClientSide) {
+ this.getAttributes().load(nbt.getList("attributes", 10));
+ }
+@@ -1371,6 +1377,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ public void setHealth(float health) {
++ // Paper start - Check for NaN
++ if (Float.isNaN(health)) { health = getMaxHealth(); if (this.valid) {
++ System.err.println("[NAN-HEALTH] " + getScoreboardName() + " had NaN health set");
++ } } // Paper end - Check for NaN
+ // CraftBukkit start - Handle scaled health
+ if (this instanceof ServerPlayer) {
+ org.bukkit.craftbukkit.entity.CraftPlayer player = ((ServerPlayer) this).getBukkitEntity();
+@@ -3847,7 +3857,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ public final void setAbsorptionAmount(float absorptionAmount) {
+- this.internalSetAbsorptionAmount(Mth.clamp(absorptionAmount, 0.0F, this.getMaxAbsorption()));
++ this.internalSetAbsorptionAmount(!Float.isNaN(absorptionAmount) ? Mth.clamp(absorptionAmount, 0.0F, this.getMaxAbsorption()) : 0.0F); // Paper - Check for NaN
+ }
+
+ protected void internalSetAbsorptionAmount(float absorptionAmount) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index dd3072717a28ae74914e0806f1874994f9ae5f5b..f32794e235fe58027bc6a13e2bbc593bbc9d713b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2350,6 +2350,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ }
+
+ public void setRealHealth(double health) {
++ if (Double.isNaN(health)) {return;} // Paper - Check for NaN
+ this.health = health;
+ }
+
diff --git a/patches/server/0074-Use-a-Shared-Random-for-Entities.patch b/patches/server/0074-Use-a-Shared-Random-for-Entities.patch
new file mode 100644
index 0000000000..8ab24ff10c
--- /dev/null
+++ b/patches/server/0074-Use-a-Shared-Random-for-Entities.patch
@@ -0,0 +1,113 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Tue, 22 Mar 2016 00:33:47 -0400
+Subject: [PATCH] Use a Shared Random for Entities
+
+Reduces memory usage and provides ensures more randomness, Especially since a lot of garbage entity objects get created.
+
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index ad16a318033bb1f24c811ad6eebe2f76eb987408..68b89c03a20c051af8c5adb63eef310905e569bc 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -184,6 +184,79 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ return tag.contains("Bukkit.updateLevel") && tag.getInt("Bukkit.updateLevel") >= level;
+ }
+
++ // Paper start - Share random for entities to make them more random
++ public static RandomSource SHARED_RANDOM = new RandomRandomSource();
++ private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource {
++ private boolean locked = false;
++
++ @Override
++ public synchronized void setSeed(long seed) {
++ if (locked) {
++ LOGGER.error("Ignoring setSeed on Entity.SHARED_RANDOM", new Throwable());
++ } else {
++ super.setSeed(seed);
++ locked = true;
++ }
++ }
++
++ @Override
++ public RandomSource fork() {
++ return new net.minecraft.world.level.levelgen.LegacyRandomSource(this.nextLong());
++ }
++
++ @Override
++ public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() {
++ return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong());
++ }
++
++ // these below are added to fix reobf issues that I don't wanna deal with right now
++ @Override
++ public int next(int bits) {
++ return super.next(bits);
++ }
++
++ @Override
++ public int nextInt(int origin, int bound) {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(origin, bound);
++ }
++
++ @Override
++ public long nextLong() {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextLong();
++ }
++
++ @Override
++ public int nextInt() {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt();
++ }
++
++ @Override
++ public int nextInt(int bound) {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(bound);
++ }
++
++ @Override
++ public boolean nextBoolean() {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextBoolean();
++ }
++
++ @Override
++ public float nextFloat() {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextFloat();
++ }
++
++ @Override
++ public double nextDouble() {
++ return net.minecraft.world.level.levelgen.BitRandomSource.super.nextDouble();
++ }
++
++ @Override
++ public double nextGaussian() {
++ return super.nextGaussian();
++ }
++ }
++ // Paper end - Share random for entities to make them more random
++
+ private CraftEntity bukkitEntity;
+
+ public CraftEntity getBukkitEntity() {
+@@ -374,7 +447,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ this.bb = Entity.INITIAL_AABB;
+ this.stuckSpeedMultiplier = Vec3.ZERO;
+ this.nextStep = 1.0F;
+- this.random = RandomSource.create();
++ this.random = SHARED_RANDOM; // Paper - Share random for entities to make them more random
+ this.remainingFireTicks = -this.getFireImmuneTicks();
+ this.fluidHeight = new Object2DoubleArrayMap(2);
+ this.fluidOnEyes = new HashSet();
+diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java
+index ba4f1f3ecee95e53cd9ed752a9249fb69228edf2..97a3f0ab3dfca24991051395229dd4c601a66fa0 100644
+--- a/src/main/java/net/minecraft/world/entity/animal/Squid.java
++++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java
+@@ -46,7 +46,7 @@ public class Squid extends AgeableWaterCreature {
+
+ public Squid(EntityType<? extends Squid> type, Level world) {
+ super(type, world);
+- this.random.setSeed((long)this.getId());
++ //this.random.setSeed((long)this.getId()); // Paper - Share random for entities to make them more random
+ this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F;
+ }
+
diff --git a/patches/server/0075-Configurable-spawn-chances-for-skeleton-horses.patch b/patches/server/0075-Configurable-spawn-chances-for-skeleton-horses.patch
new file mode 100644
index 0000000000..a3c2dccb23
--- /dev/null
+++ b/patches/server/0075-Configurable-spawn-chances-for-skeleton-horses.patch
@@ -0,0 +1,19 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Tue, 22 Mar 2016 12:04:28 -0500
+Subject: [PATCH] Configurable spawn chances for skeleton horses
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index db57ac5086f862833057a06ff1253934ef46230d..b3939c0da364492e60e3050be0c314e00b935019 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -600,7 +600,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ if (this.isRainingAt(blockposition)) {
+ DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition);
+- boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * 0.01D && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD);
++ boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01D) && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper - Configurable spawn chances for skeleton horses
+
+ if (flag1) {
+ SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this, EntitySpawnReason.EVENT);
diff --git a/patches/server/0076-Only-process-BlockPhysicsEvent-if-a-plugin-has-a-lis.patch b/patches/server/0076-Only-process-BlockPhysicsEvent-if-a-plugin-has-a-lis.patch
new file mode 100644
index 0000000000..4c0ca33b70
--- /dev/null
+++ b/patches/server/0076-Only-process-BlockPhysicsEvent-if-a-plugin-has-a-lis.patch
@@ -0,0 +1,70 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 28 Mar 2016 19:55:45 -0400
+Subject: [PATCH] Only process BlockPhysicsEvent if a plugin has a listener
+
+Saves on some object allocation and processing when no plugin listens to this
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 9c26725e12adb2d17b9fa27f632fbad02e904c9a..b293c7f901b4f0c6e55bc3edaab1eddb72c1218f 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -1584,6 +1584,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ while (iterator.hasNext()) {
+ ServerLevel worldserver = (ServerLevel) iterator.next();
++ worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
+
+ gameprofilerfiller.push(() -> {
+ String s = String.valueOf(worldserver);
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index b3939c0da364492e60e3050be0c314e00b935019..1dcdb40c1d3def6e3fb1628100d74bf77a45efe3 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -229,6 +229,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ // CraftBukkit start
+ public final LevelStorageSource.LevelStorageAccess convertable;
+ public final UUID uuid;
++ public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
+
+ public LevelChunk getChunkIfLoaded(int x, int z) {
+ return this.chunkSource.getChunk(x, z, false);
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 36ed95a432429d7fe977684d26818afdb49c36dc..fbc2830aab0bfa35ff071bbee84ce00da2d0e405 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -490,7 +490,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ // CraftBukkit start
+ iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam
+ CraftWorld world = ((ServerLevel) this).getWorld();
+- if (world != null) {
++ if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent
+ BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata));
+ this.getCraftServer().getPluginManager().callEvent(event);
+
+diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java
+index 4db8f94dc279d05ed1cdf52e49ef780025828067..eb324fda54ada3ed7941713a784ed2d686ec8c4b 100644
+--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java
+@@ -31,7 +31,7 @@ public abstract class BushBlock extends Block {
+ // CraftBukkit start
+ if (!state.canSurvive(world, pos)) {
+ // Suppress during worldgen
+- if (!(world instanceof Level world1) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) {
++ if (!(world instanceof net.minecraft.server.level.ServerLevel world1 && world1.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) { // Paper
+ return Blocks.AIR.defaultBlockState();
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
+index 46047e0567af62612aad479fff6bea88903f108a..edb3b6cdb617c48140539728af1373993e78648f 100644
+--- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
+@@ -104,7 +104,7 @@ public class DoublePlantBlock extends BushBlock {
+
+ protected static void preventDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) {
+ // CraftBukkit start
+- if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) {
++ if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper
+ return;
+ }
+ // CraftBukkit end
diff --git a/patches/server/0077-Entity-AddTo-RemoveFrom-World-Events.patch b/patches/server/0077-Entity-AddTo-RemoveFrom-World-Events.patch
new file mode 100644
index 0000000000..0bb1405b36
--- /dev/null
+++ b/patches/server/0077-Entity-AddTo-RemoveFrom-World-Events.patch
@@ -0,0 +1,26 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 28 Mar 2016 20:32:58 -0400
+Subject: [PATCH] Entity AddTo/RemoveFrom World Events
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 1dcdb40c1d3def6e3fb1628100d74bf77a45efe3..c0e4c3fe5e996f84dcbaa2d78f5b845147e5ae84 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -2205,6 +2205,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ entity.setOrigin(entity.getOriginVector().toLocation(getWorld()));
+ }
+ // Paper end - Entity origin API
++ new com.destroystokyo.paper.event.entity.EntityAddToWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
+ }
+
+ public void onTrackingEnd(Entity entity) {
+@@ -2275,6 +2276,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ }
+ }
+ // CraftBukkit end
++ new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
+ }
+
+ public void onSectionChange(Entity entity) {
diff --git a/patches/server/0078-Configurable-Chunk-Inhabited-Time.patch b/patches/server/0078-Configurable-Chunk-Inhabited-Time.patch
new file mode 100644
index 0000000000..74892b6dae
--- /dev/null
+++ b/patches/server/0078-Configurable-Chunk-Inhabited-Time.patch
@@ -0,0 +1,30 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 28 Mar 2016 20:46:14 -0400
+Subject: [PATCH] Configurable Chunk Inhabited Time
+
+Vanilla stores how long a chunk has been active on a server, and dynamically scales some
+aspects of vanilla gameplay to this factor.
+
+For people who want all chunks to be treated equally, you can chose a fixed value.
+
+This allows to fine-tune vanilla gameplay.
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index 95da29b973d43b59d9c4d0c83068dc74b59c9c8b..31696856600db18d1dc401b7fa72a7c9ff219304 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -200,6 +200,13 @@ public class LevelChunk extends ChunkAccess {
+ return new ChunkAccess.PackedTicks(this.blockTicks.pack(time), this.fluidTicks.pack(time));
+ }
+
++ // Paper start
++ @Override
++ public long getInhabitedTime() {
++ return this.level.paperConfig().chunks.fixedChunkInhabitedTime < 0 ? super.getInhabitedTime() : this.level.paperConfig().chunks.fixedChunkInhabitedTime;
++ }
++ // Paper end
++
+ @Override
+ public GameEventListenerRegistry getListenerRegistry(int ySectionCoord) {
+ Level world = this.level;
diff --git a/patches/server/0079-EntityPathfindEvent.patch b/patches/server/0079-EntityPathfindEvent.patch
new file mode 100644
index 0000000000..82ced28d8a
--- /dev/null
+++ b/patches/server/0079-EntityPathfindEvent.patch
@@ -0,0 +1,154 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 28 Mar 2016 21:22:26 -0400
+Subject: [PATCH] EntityPathfindEvent
+
+Fires when an Entity decides to start moving to a location.
+
+diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
+index b81a5149720daf23d3d33f0aaae51216121ea2e2..2bd66da93227d4e4fc2ec4df47ae94b17f4d39d3 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
+@@ -39,7 +39,7 @@ public class FlyingPathNavigation extends PathNavigation {
+
+ @Override
+ public Path createPath(Entity entity, int distance) {
+- return this.createPath(entity.blockPosition(), distance);
++ return this.createPath(entity.blockPosition(), entity, distance); // Paper - EntityPathfindEvent
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
+index 90a7940bf8b32cb245e9253d957cb437fa600857..2796df7af365c452b28373adfd7daf1d6730bac5 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
+@@ -41,7 +41,7 @@ public class GroundPathNavigation extends PathNavigation {
+ }
+
+ @Override
+- public Path createPath(BlockPos target, int distance) {
++ public Path createPath(BlockPos target, @javax.annotation.Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent
+ LevelChunk levelChunk = this.level
+ .getChunkSource()
+ .getChunkNow(SectionPos.blockToSectionCoord(target.getX()), SectionPos.blockToSectionCoord(target.getZ()));
+@@ -56,7 +56,7 @@ public class GroundPathNavigation extends PathNavigation {
+ }
+
+ if (mutableBlockPos.getY() > this.level.getMinY()) {
+- return super.createPath(mutableBlockPos.above(), distance);
++ return super.createPath(mutableBlockPos.above(), entity, distance); // Paper - EntityPathfindEvent
+ }
+
+ mutableBlockPos.setY(target.getY() + 1);
+@@ -69,7 +69,7 @@ public class GroundPathNavigation extends PathNavigation {
+ }
+
+ if (!levelChunk.getBlockState(target).isSolid()) {
+- return super.createPath(target, distance);
++ return super.createPath(target, entity, distance); // Paper - EntityPathfindEvent
+ } else {
+ BlockPos.MutableBlockPos mutableBlockPos2 = target.mutable().move(Direction.UP);
+
+@@ -77,14 +77,14 @@ public class GroundPathNavigation extends PathNavigation {
+ mutableBlockPos2.move(Direction.UP);
+ }
+
+- return super.createPath(mutableBlockPos2.immutable(), distance);
++ return super.createPath(mutableBlockPos2.immutable(), entity, distance); // Paper - EntityPathfindEvent
+ }
+ }
+ }
+
+ @Override
+ public Path createPath(Entity entity, int distance) {
+- return this.createPath(entity.blockPosition(), distance);
++ return this.createPath(entity.blockPosition(), entity, distance); // Paper - EntityPathfindEvent
+ }
+
+ private int getSurfaceY() {
+diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
+index 7d3cb358fe543253e988531df3434ae1274814d3..1d5ce4caf99a3fb376b350968a6bd1ac8471ffec 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
+@@ -125,7 +125,13 @@ public abstract class PathNavigation {
+
+ @Nullable
+ public Path createPath(BlockPos target, int distance) {
+- return this.createPath(ImmutableSet.of(target), 8, false, distance);
++ // Paper start - EntityPathfindEvent
++ return this.createPath(target, null, distance);
++ }
++ @Nullable
++ public Path createPath(BlockPos target, @Nullable Entity entity, int distance) {
++ return this.createPath(ImmutableSet.of(target), entity, 8, false, distance);
++ // Paper end - EntityPathfindEvent
+ }
+
+ @Nullable
+@@ -135,7 +141,7 @@ public abstract class PathNavigation {
+
+ @Nullable
+ public Path createPath(Entity entity, int distance) {
+- return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, distance);
++ return this.createPath(ImmutableSet.of(entity.blockPosition()), entity, 16, true, distance); // Paper - EntityPathfindEvent
+ }
+
+ @Nullable
+@@ -145,6 +151,17 @@ public abstract class PathNavigation {
+
+ @Nullable
+ protected Path createPath(Set<BlockPos> positions, int range, boolean useHeadPos, int distance, float followRange) {
++ // Paper start - EntityPathfindEvent
++ return this.createPath(positions, null, range, useHeadPos, distance, followRange);
++ }
++
++ @Nullable
++ protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance) {
++ return this.createPath(positions, target, range, useHeadPos, distance, (float) this.mob.getAttributeValue(Attributes.FOLLOW_RANGE));
++ }
++
++ @Nullable protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance, float followRange) {
++ // Paper end - EntityPathfindEvent
+ if (positions.isEmpty()) {
+ return null;
+ } else if (this.mob.getY() < (double)this.level.getMinY()) {
+@@ -154,6 +171,23 @@ public abstract class PathNavigation {
+ } else if (this.path != null && !this.path.isDone() && positions.contains(this.targetPos)) {
+ return this.path;
+ } else {
++ // Paper start - EntityPathfindEvent
++ boolean copiedSet = false;
++ for (BlockPos possibleTarget : positions) {
++ if (!new com.destroystokyo.paper.event.entity.EntityPathfindEvent(this.mob.getBukkitEntity(),
++ io.papermc.paper.util.MCUtil.toLocation(this.mob.level(), possibleTarget), target == null ? null : target.getBukkitEntity()).callEvent()) {
++ if (!copiedSet) {
++ copiedSet = true;
++ positions = new java.util.HashSet<>(positions);
++ }
++ // note: since we copy the set this remove call is safe, since we're iterating over the old copy
++ positions.remove(possibleTarget);
++ if (positions.isEmpty()) {
++ return null;
++ }
++ }
++ }
++ // Paper end - EntityPathfindEvent
+ ProfilerFiller profilerFiller = Profiler.get();
+ profilerFiller.push("pathfind");
+ BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition();
+diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
+index 1a1bc30b425858d82dbfb84b4a94d7793cab7125..5bbfa43d1e97970f035fcb101c3252c01ffead0d 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
+@@ -16,9 +16,9 @@ public class WallClimberNavigation extends GroundPathNavigation {
+ }
+
+ @Override
+- public Path createPath(BlockPos target, int distance) {
++ public Path createPath(BlockPos target, @Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent
+ this.pathToPosition = target;
+- return super.createPath(target, distance);
++ return super.createPath(target, entity, distance); // Paper - EntityPathfindEvent
+ }
+
+ @Override
diff --git a/patches/server/0080-Sanitise-RegionFileCache-and-make-configurable.patch b/patches/server/0080-Sanitise-RegionFileCache-and-make-configurable.patch
new file mode 100644
index 0000000000..c8504969b8
--- /dev/null
+++ b/patches/server/0080-Sanitise-RegionFileCache-and-make-configurable.patch
@@ -0,0 +1,25 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Antony Riley <[email protected]>
+Date: Tue, 29 Mar 2016 08:22:55 +0300
+Subject: [PATCH] Sanitise RegionFileCache and make configurable
+
+RegionFileCache prior to this patch would close every single open region
+file upon reaching a size of 256.
+This patch modifies that behaviour so it closes the the least recently
+used RegionFile.
+The implementation uses a LinkedHashMap as an LRU cache (modified from HashMap).
+The maximum size of the RegionFileCache is also made configurable.
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index 40f2f4d052add3b4270d29c843e49fb621e1bc8d..54803d561d98b0d3fd8aa2060a7a184a00dd9da6 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -39,7 +39,7 @@ public final class RegionFileStorage implements AutoCloseable {
+ if (regionfile != null) {
+ return regionfile;
+ } else {
+- if (this.regionCache.size() >= 256) {
++ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
+ ((RegionFile) this.regionCache.removeLast()).close();
+ }
+
diff --git a/patches/server/0081-Do-not-load-chunks-for-Pathfinding.patch b/patches/server/0081-Do-not-load-chunks-for-Pathfinding.patch
new file mode 100644
index 0000000000..f67f87234b
--- /dev/null
+++ b/patches/server/0081-Do-not-load-chunks-for-Pathfinding.patch
@@ -0,0 +1,24 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 31 Mar 2016 19:17:58 -0400
+Subject: [PATCH] Do not load chunks for Pathfinding
+
+
+diff --git a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
+index 448e4d68790e795e1848236e700bf6955098e0d9..c84fd369d92932903c76bb2012602617d3e2d213 100644
+--- a/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
++++ b/src/main/java/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
+@@ -478,7 +478,12 @@ public class WalkNodeEvaluator extends NodeEvaluator {
+ }
+
+ protected static PathType getPathTypeFromState(BlockGetter world, BlockPos pos) {
+- BlockState blockState = world.getBlockState(pos);
++ // Paper start - Do not load chunks during pathfinding
++ BlockState blockState = world.getBlockStateIfLoaded(pos);
++ if (blockState == null) {
++ return PathType.BLOCKED;
++ }
++ // Paper end
+ Block block = blockState.getBlock();
+ if (blockState.isAir()) {
+ return PathType.OPEN;
diff --git a/patches/server/0082-Add-PlayerUseUnknownEntityEvent.patch b/patches/server/0082-Add-PlayerUseUnknownEntityEvent.patch
new file mode 100644
index 0000000000..9080ba1334
--- /dev/null
+++ b/patches/server/0082-Add-PlayerUseUnknownEntityEvent.patch
@@ -0,0 +1,78 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jedediah Smith <[email protected]>
+Date: Sat, 2 Apr 2016 05:09:16 -0400
+Subject: [PATCH] Add PlayerUseUnknownEntityEvent
+
+Adds the PlayerUseUnknownEntityEvent to be used by plugins dealing with
+virtual entities/entities that are not actually known to the server.
+
+Co-authored-by: Nassim Jahnke <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundInteractPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
+index 1e9c68cd1868d083e6a790d56006dd4aa432010a..8a0ee9564fc36a2badf1357f7e6c47b5f1500f6c 100644
+--- a/src/main/java/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
+@@ -176,4 +176,14 @@ public class ServerboundInteractPacket implements Packet<ServerGamePacketListene
+ buf.writeEnum(this.hand);
+ }
+ }
++
++ // Paper start - PlayerUseUnknownEntityEvent
++ public int getEntityId() {
++ return this.entityId;
++ }
++
++ public boolean isAttack() {
++ return this.action.getType() == ActionType.ATTACK;
++ }
++ // Paper end - PlayerUseUnknownEntityEvent
+ }
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index 97b776e384e8ce064ea9bb93fe24d902ff2d8817..f8e38d3334eca60cd5abe1838b1f274a82c8ede5 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -2585,7 +2585,26 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ });
+ }
+ }
++ // Paper start - PlayerUseUnknownEntityEvent
++ else {
++ packet.dispatch(new net.minecraft.network.protocol.game.ServerboundInteractPacket.Handler() {
++ @Override
++ public void onInteraction(net.minecraft.world.InteractionHand hand) {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, null);
++ }
++
++ @Override
++ public void onInteraction(net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.Vec3 pos) {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, pos);
++ }
+
++ @Override
++ public void onAttack() {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, net.minecraft.world.InteractionHand.MAIN_HAND, null);
++ }
++ });
++ }
++ // Paper end - PlayerUseUnknownEntityEvent
+ }
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+index 8ea4d63833cd1500d7f413f761aa9a7cf26520c0..9100ea65e85a0e55cad736634fa63815366334a8 100644
+--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+@@ -1929,4 +1929,13 @@ public class CraftEventFactory {
+
+ Bukkit.getPluginManager().callEvent(new EntityRemoveEvent(entity.getBukkitEntity(), cause));
+ }
++ // Paper start - PlayerUseUnknownEntityEvent
++ public static void callPlayerUseUnknownEntityEvent(net.minecraft.world.entity.player.Player player, net.minecraft.network.protocol.game.ServerboundInteractPacket packet, InteractionHand hand, @Nullable net.minecraft.world.phys.Vec3 vector) {
++ new com.destroystokyo.paper.event.player.PlayerUseUnknownEntityEvent(
++ (Player) player.getBukkitEntity(), packet.getEntityId(), packet.isAttack(),
++ CraftEquipmentSlot.getHand(hand),
++ vector != null ? CraftVector.toBukkit(vector) : null
++ ).callEvent();
++ }
++ // Paper end - PlayerUseUnknownEntityEvent
+ }
diff --git a/patches/server/0083-Configurable-random-tick-rates-for-blocks.patch b/patches/server/0083-Configurable-random-tick-rates-for-blocks.patch
new file mode 100644
index 0000000000..c1bbe4c616
--- /dev/null
+++ b/patches/server/0083-Configurable-random-tick-rates-for-blocks.patch
@@ -0,0 +1,35 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sun, 3 Apr 2016 16:28:17 -0400
+Subject: [PATCH] Configurable random tick rates for blocks
+
+A general purpose patch that includes config options for the tick rate
+of a variety of blocks that are random ticked.
+
+Co-authored-by: MrPowerGamerBR <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java
+index 38b3c14d393137026720f42bd9f14b856b8377d0..a87f8345aa5520a867a8dd769b43526b20b8c16a 100644
+--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java
+@@ -93,6 +93,8 @@ public class FarmBlock extends Block {
+ @Override
+ protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ int i = (Integer) state.getValue(FarmBlock.MOISTURE);
++ if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
++ if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
+
+ if (!FarmBlock.isNearWater(world, pos) && !world.isRainingAt(pos.above())) {
+ if (i > 0) {
+diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+index 63fb3b192a02af585168dda58f5a49ff2f6bc216..30047cbef0ef54ad326e71761cba64887624d493 100644
+--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+@@ -43,6 +43,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
+
+ @Override
+ protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
++ if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks
+ if (!SpreadingSnowyDirtBlock.canBeGrass(state, world, pos)) {
+ // CraftBukkit start
+ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
diff --git a/patches/server/0084-Fix-Cancelling-BlockPlaceEvent-triggering-physics.patch b/patches/server/0084-Fix-Cancelling-BlockPlaceEvent-triggering-physics.patch
new file mode 100644
index 0000000000..469ff2ff1a
--- /dev/null
+++ b/patches/server/0084-Fix-Cancelling-BlockPlaceEvent-triggering-physics.patch
@@ -0,0 +1,24 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sun, 3 Apr 2016 17:48:50 -0400
+Subject: [PATCH] Fix Cancelling BlockPlaceEvent triggering physics
+
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index c0e4c3fe5e996f84dcbaa2d78f5b845147e5ae84..7611f58246960ec3fd0521ab5f1743a5b08d8241 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -1378,11 +1378,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ @Override
+ public void updateNeighborsAt(BlockPos pos, Block block) {
++ if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
+ this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, (Direction) null, (Direction) null));
+ }
+
+ @Override
+ public void updateNeighborsAt(BlockPos pos, Block sourceBlock, @Nullable Orientation orientation) {
++ if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
+ this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null, orientation);
+ }
+
diff --git a/patches/server/0085-Optimize-DataBits.patch b/patches/server/0085-Optimize-DataBits.patch
new file mode 100644
index 0000000000..fa5c0b02f0
--- /dev/null
+++ b/patches/server/0085-Optimize-DataBits.patch
@@ -0,0 +1,118 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Tue, 5 Apr 2016 21:38:58 -0400
+Subject: [PATCH] Optimize DataBits
+
+Remove Debug checks as these are super hot and causing noticeable hits
+
+Before: http://i.imgur.com/nQsMzAE.png
+After: http://i.imgur.com/nJ46crB.png
+
+Optimize redundant converting of static fields into an unsigned long each call by precomputing it in ctor
+
+diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java
+index dea4f322f750a0a09407fdb48d5d6e809dfe8ed4..9f438d9c6eb05e43d24e4af68188a3d4c46a938c 100644
+--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java
+@@ -204,8 +204,8 @@ public class SimpleBitStorage implements BitStorage {
+ private final long mask;
+ private final int size;
+ private final int valuesPerLong;
+- private final int divideMul;
+- private final int divideAdd;
++ private final int divideMul; private final long divideMulUnsigned; // Paper - Perf: Optimize SimpleBitStorage; referenced in b(int) with 2 Integer.toUnsignedLong calls
++ private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage
+ private final int divideShift;
+
+ public SimpleBitStorage(int elementBits, int size, int[] data) {
+@@ -248,8 +248,8 @@ public class SimpleBitStorage implements BitStorage {
+ this.mask = (1L << elementBits) - 1L;
+ this.valuesPerLong = (char)(64 / elementBits);
+ int i = 3 * (this.valuesPerLong - 1);
+- this.divideMul = MAGIC[i + 0];
+- this.divideAdd = MAGIC[i + 1];
++ this.divideMul = MAGIC[i + 0]; this.divideMulUnsigned = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
++ this.divideAdd = MAGIC[i + 1]; this.divideAddUnsigned = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
+ this.divideShift = MAGIC[i + 2];
+ int j = (size + this.valuesPerLong - 1) / this.valuesPerLong;
+ if (data != null) {
+@@ -264,15 +264,15 @@ public class SimpleBitStorage implements BitStorage {
+ }
+
+ private int cellIndex(int index) {
+- long l = Integer.toUnsignedLong(this.divideMul);
+- long m = Integer.toUnsignedLong(this.divideAdd);
+- return (int)((long)index * l + m >> 32 >> this.divideShift);
++ //long l = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
++ //long m = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
++ return (int) (index * this.divideMulUnsigned + this.divideAddUnsigned >> 32 >> this.divideShift); // Paper - Perf: Optimize SimpleBitStorage
+ }
+
+ @Override
+- public int getAndSet(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, this.mask, (long)value);
++ public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
+@@ -282,9 +282,9 @@ public class SimpleBitStorage implements BitStorage {
+ }
+
+ @Override
+- public void set(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, this.mask, (long)value);
++ public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
+@@ -292,8 +292,8 @@ public class SimpleBitStorage implements BitStorage {
+ }
+
+ @Override
+- public int get(int index) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
++ public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
+diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java
+index 8dd5a5899e9b5d8b3f1a6064dd7c1580313da69e..50040c497a819cd1229042ab3cb057d34a32cacc 100644
+--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
+@@ -13,21 +13,21 @@ public class ZeroBitStorage implements BitStorage {
+ }
+
+ @Override
+- public int getAndSet(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, 0L, (long)value);
++ public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, 0L, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ return 0;
+ }
+
+ @Override
+- public void set(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, 0L, (long)value);
++ public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, 0L, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ }
+
+ @Override
+- public int get(int index) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
++ public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
+ return 0;
+ }
+
diff --git a/patches/server/0086-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch b/patches/server/0086-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
new file mode 100644
index 0000000000..4a2793dc00
--- /dev/null
+++ b/patches/server/0086-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
@@ -0,0 +1,50 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Wed, 6 Apr 2016 01:04:23 -0500
+Subject: [PATCH] Option to use vanilla per-world scoreboard coloring on names
+
+This change is basically a bandaid to fix CB's complete and utter lack
+of support for vanilla scoreboard name modifications.
+
+In the future, finding a way to merge the vanilla expectations in with
+bukkit's concept of a display name would be preferable. There was a PR
+for this on CB at one point but I can't find it. We may need to do this
+ourselves at some point in the future.
+
+diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+index b9a64a40dbb025e34a3de81df1208de45df3cfcc..14e412ebf75b0e06ab53a1c8f9dd1be6ad1e2680 100644
+--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+@@ -20,6 +20,7 @@ import net.kyori.adventure.audience.ForwardingAudience;
+ import net.kyori.adventure.key.Key;
+ import net.kyori.adventure.text.Component;
+ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.minecraft.ChatFormatting;
+ import net.minecraft.Optionull;
+ import net.minecraft.Util;
+ import net.minecraft.core.registries.Registries;
+@@ -31,6 +32,7 @@ import net.minecraft.resources.ResourceLocation;
+ import net.minecraft.server.MinecraftServer;
+ import net.minecraft.server.level.ServerPlayer;
+ import org.bukkit.command.ConsoleCommandSender;
++import org.bukkit.craftbukkit.CraftWorld;
+ import org.bukkit.craftbukkit.entity.CraftPlayer;
+ import org.bukkit.craftbukkit.util.LazyPlayerSet;
+ import org.bukkit.craftbukkit.util.Waitable;
+@@ -329,10 +331,16 @@ public final class ChatProcessor {
+ }
+
+ static String legacyDisplayName(final CraftPlayer player) {
++ if (((org.bukkit.craftbukkit.CraftWorld) player.getWorld()).getHandle().paperConfig().scoreboards.useVanillaWorldScoreboardNameColoring) {
++ return legacySection().serialize(player.teamDisplayName()) + ChatFormatting.RESET;
++ }
+ return player.getDisplayName();
+ }
+
+ static Component displayName(final CraftPlayer player) {
++ if (((CraftWorld) player.getWorld()).getHandle().paperConfig().scoreboards.useVanillaWorldScoreboardNameColoring) {
++ return player.teamDisplayName();
++ }
+ return player.displayName();
+ }
+
diff --git a/patches/server/0087-Configurable-Player-Collision.patch b/patches/server/0087-Configurable-Player-Collision.patch
new file mode 100644
index 0000000000..381fa85bb4
--- /dev/null
+++ b/patches/server/0087-Configurable-Player-Collision.patch
@@ -0,0 +1,114 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Wed, 13 Apr 2016 02:10:49 -0400
+Subject: [PATCH] Configurable Player Collision
+
+
+diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
+index 9a1a961eabd4362c171da78c6be82c867f3696a4..1d0c473442b5c72245c356054440323e3c5d4711 100644
+--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
+@@ -200,7 +200,7 @@ public class ClientboundSetPlayerTeamPacket implements Packet<ClientGamePacketLi
+ ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.displayName);
+ buf.writeByte(this.options);
+ buf.writeUtf(this.nametagVisibility);
+- buf.writeUtf(this.collisionRule);
++ buf.writeUtf(!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions ? "never" : this.collisionRule); // Paper - Configurable player collision
+ buf.writeEnum(this.color);
+ ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.playerPrefix);
+ ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.playerSuffix);
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index b293c7f901b4f0c6e55bc3edaab1eddb72c1218f..d374ff51987c30a84e137dd623e0f64966999b63 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -653,6 +653,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
+ }
+
++ // Paper start - Configurable player collision; Handle collideRule team for player collision toggle
++ final ServerScoreboard scoreboard = this.getScoreboard();
++ final java.util.Collection<String> toRemove = scoreboard.getPlayerTeams().stream().filter(team -> team.getName().startsWith("collideRule_")).map(net.minecraft.world.scores.PlayerTeam::getName).collect(java.util.stream.Collectors.toList());
++ for (String teamName : toRemove) {
++ scoreboard.removePlayerTeam(scoreboard.getPlayerTeam(teamName)); // Clean up after ourselves
++ }
++
++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions) {
++ this.getPlayerList().collideRuleTeamName = org.apache.commons.lang3.StringUtils.left("collideRule_" + java.util.concurrent.ThreadLocalRandom.current().nextInt(), 16);
++ net.minecraft.world.scores.PlayerTeam collideTeam = scoreboard.addPlayerTeam(this.getPlayerList().collideRuleTeamName);
++ collideTeam.setSeeFriendlyInvisibles(false); // Because we want to mimic them not being on a team at all
++ }
++ // Paper end - Configurable player collision
++
+ this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
+ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
+ this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 7782f26764ef79968b1e2f5e1f27f1162ed122de..005e63978a306c695e21b26498937a3b81fb2b38 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -159,6 +159,7 @@ public abstract class PlayerList {
+ // CraftBukkit start
+ private CraftServer cserver;
+ private final Map<String,ServerPlayer> playersByName = new java.util.HashMap<>();
++ public @Nullable String collideRuleTeamName; // Paper - Configurable player collision
+
+ public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registryManager, PlayerDataStorage saveHandler, int maxPlayers) {
+ this.cserver = server.server = new CraftServer((DedicatedServer) server, this);
+@@ -348,6 +349,13 @@ public abstract class PlayerList {
+ player.loadAndSpawnParentVehicle(optional);
+ player.initInventoryMenu();
+ // CraftBukkit - Moved from above, added world
++ // Paper start - Configurable player collision; Add to collideRule team if needed
++ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
++ final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName);
++ if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) {
++ scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
++ }
++ // Paper end - Configurable player collision
+ PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ());
+ }
+
+@@ -470,6 +478,16 @@ public abstract class PlayerList {
+ entityplayer.doTick(); // SPIGOT-924
+ // CraftBukkit end
+
++ // Paper start - Configurable player collision; Remove from collideRule team if needed
++ if (this.collideRuleTeamName != null) {
++ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
++ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
++ if (entityplayer.getTeam() == team && team != null) {
++ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team);
++ }
++ }
++ // Paper end - Configurable player collision
++
+ this.save(entityplayer);
+ if (entityplayer.isPassenger()) {
+ Entity entity = entityplayer.getRootVehicle();
+@@ -1096,6 +1114,13 @@ public abstract class PlayerList {
+ }
+ // CraftBukkit end
+
++ // Paper start - Configurable player collision; Remove collideRule team if it exists
++ if (this.collideRuleTeamName != null) {
++ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
++ final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName);
++ if (team != null) scoreboard.removePlayerTeam(team);
++ }
++ // Paper end - Configurable player collision
+ }
+
+ // CraftBukkit start
+diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java
+index a617ea34cfc28cefd68dd14ffbb334b87f98f65c..3a4c1d4afddd7d8d1f43554a7a08855686cadf56 100644
+--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java
++++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java
+@@ -50,7 +50,7 @@ public final class EntitySelector {
+ return (Predicate) (scoreboardteambase_enumteampush == Team.CollisionRule.NEVER ? Predicates.alwaysFalse() : EntitySelector.NO_SPECTATORS.and((entity1) -> {
+ if (!entity1.canCollideWithBukkit(entity) || !entity.canCollideWithBukkit(entity1)) { // CraftBukkit - collidable API
+ return false;
+- } else if (entity.level().isClientSide && (!(entity1 instanceof Player) || !((Player) entity1).isLocalPlayer())) {
++ } else if (entity1 instanceof Player && entity instanceof Player && !io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions) { // Paper - Configurable player collision
+ return false;
+ } else {
+ PlayerTeam scoreboardteam1 = entity1.getTeam();
diff --git a/patches/server/0088-Add-handshake-event-to-allow-plugins-to-handle-clien.patch b/patches/server/0088-Add-handshake-event-to-allow-plugins-to-handle-clien.patch
new file mode 100644
index 0000000000..33ef52f088
--- /dev/null
+++ b/patches/server/0088-Add-handshake-event-to-allow-plugins-to-handle-clien.patch
@@ -0,0 +1,56 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kashike <[email protected]>
+Date: Wed, 13 Apr 2016 20:21:38 -0700
+Subject: [PATCH] Add handshake event to allow plugins to handle client
+ handshaking logic themselves
+
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
+index 64e3d3626a30d4dfe15ed1f7cb3f0480ece5edce..7ae4279768b70a4fdc8f4438898871a17c8fe402 100644
+--- a/src/main/java/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
+@@ -121,9 +121,43 @@ public class ServerHandshakePacketListenerImpl implements ServerHandshakePacketL
+ this.connection.disconnect((Component) ichatmutablecomponent);
+ } else {
+ this.connection.setupInboundProtocol(LoginProtocols.SERVERBOUND, new ServerLoginPacketListenerImpl(this.server, this.connection, transfer));
++ // Paper start - PlayerHandshakeEvent
++ boolean proxyLogicEnabled = org.spigotmc.SpigotConfig.bungee;
++ boolean handledByEvent = false;
++ // Try and handle the handshake through the event
++ if (com.destroystokyo.paper.event.player.PlayerHandshakeEvent.getHandlerList().getRegisteredListeners().length != 0) { // Hello? Can you hear me?
++ java.net.SocketAddress socketAddress = this.connection.address;
++ String hostnameOfRemote = socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getHostString() : InetAddress.getLoopbackAddress().getHostAddress();
++ com.destroystokyo.paper.event.player.PlayerHandshakeEvent event = new com.destroystokyo.paper.event.player.PlayerHandshakeEvent(packet.hostName(), hostnameOfRemote, !proxyLogicEnabled);
++ if (event.callEvent()) {
++ // If we've failed somehow, let the client know so and go no further.
++ if (event.isFailed()) {
++ Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.failMessage());
++ this.connection.send(new ClientboundLoginDisconnectPacket(component));
++ this.connection.disconnect(component);
++ return;
++ }
++
++ if (event.getServerHostname() != null) {
++ // change hostname
++ packet = new ClientIntentionPacket(
++ packet.protocolVersion(),
++ event.getServerHostname(),
++ packet.port(),
++ packet.intention()
++ );
++ }
++ if (event.getSocketAddressHostname() != null) this.connection.address = new java.net.InetSocketAddress(event.getSocketAddressHostname(), socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getPort() : 0);
++ this.connection.spoofedUUID = event.getUniqueId();
++ this.connection.spoofedProfile = gson.fromJson(event.getPropertiesJson(), com.mojang.authlib.properties.Property[].class);
++ handledByEvent = true; // Hooray, we did it!
++ }
++ }
++ // Paper end
+ // Spigot Start
+ String[] split = packet.hostName().split("\00");
+- if (org.spigotmc.SpigotConfig.bungee) {
++ if (!handledByEvent && proxyLogicEnabled) { // Paper
++ // if (org.spigotmc.SpigotConfig.bungee) { // Paper - comment out, we check above!
+ if ( ( split.length == 3 || split.length == 4 ) && ( ServerHandshakePacketListenerImpl.HOST_PATTERN.matcher( split[1] ).matches() ) ) {
+ this.connection.hostname = split[0];
+ this.connection.address = new java.net.InetSocketAddress(split[1], ((java.net.InetSocketAddress) this.connection.getRemoteAddress()).getPort());
diff --git a/patches/server/0089-Configurable-RCON-IP-address.patch b/patches/server/0089-Configurable-RCON-IP-address.patch
new file mode 100644
index 0000000000..21e0b530fd
--- /dev/null
+++ b/patches/server/0089-Configurable-RCON-IP-address.patch
@@ -0,0 +1,47 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sat, 16 Apr 2016 00:39:33 -0400
+Subject: [PATCH] Configurable RCON IP address
+
+For servers with multiple IP's, ability to bind to a specific interface.
+
+== AT ==
+public net.minecraft.server.dedicated.Settings getStringRaw(Ljava/lang/String;)Ljava/lang/String;
+
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java
+index 353cc3622c7a802bf130146964610e66eb431d64..83d279a66484dfeef3ce34bef3d1c8f221c67f6d 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java
+@@ -110,6 +110,8 @@ public class DedicatedServerProperties extends Settings<DedicatedServerPropertie
+ public final WorldOptions worldOptions;
+ public boolean acceptsTransfers;
+
++ public final String rconIp; // Paper - Configurable rcon ip
++
+ // CraftBukkit start
+ public DedicatedServerProperties(Properties properties, OptionSet optionset) {
+ super(properties, optionset);
+@@ -172,6 +174,10 @@ public class DedicatedServerProperties extends Settings<DedicatedServerPropertie
+ }, WorldPresets.NORMAL.location().toString()));
+ this.serverResourcePackInfo = DedicatedServerProperties.getServerPackInfo(this.get("resource-pack-id", ""), this.get("resource-pack", ""), this.get("resource-pack-sha1", ""), this.getLegacyString("resource-pack-hash"), this.get("require-resource-pack", false), this.get("resource-pack-prompt", ""));
+ this.initialDataPackConfiguration = DedicatedServerProperties.getDatapackConfig(this.get("initial-enabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getEnabled())), this.get("initial-disabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getDisabled())));
++ // Paper start - Configurable rcon ip
++ final String rconIp = this.getStringRaw("rcon.ip");
++ this.rconIp = rconIp == null ? this.serverIp : rconIp;
++ // Paper end - Configurable rcon ip
+ }
+
+ // CraftBukkit start
+diff --git a/src/main/java/net/minecraft/server/rcon/thread/RconThread.java b/src/main/java/net/minecraft/server/rcon/thread/RconThread.java
+index cf6ff562614e07dfdc0ae1d2d68df67d61136df0..594fbb033b63b8c9fb8752b1fcc78f8e9f7a2a83 100644
+--- a/src/main/java/net/minecraft/server/rcon/thread/RconThread.java
++++ b/src/main/java/net/minecraft/server/rcon/thread/RconThread.java
+@@ -57,7 +57,7 @@ public class RconThread extends GenericThread {
+ @Nullable
+ public static RconThread create(ServerInterface server) {
+ DedicatedServerProperties dedicatedServerProperties = server.getProperties();
+- String string = server.getServerIp();
++ String string = dedicatedServerProperties.rconIp; // Paper - Configurable rcon ip
+ if (string.isEmpty()) {
+ string = "0.0.0.0";
+ }
diff --git a/patches/server/0090-EntityRegainHealthEvent-isFastRegen-API.patch b/patches/server/0090-EntityRegainHealthEvent-isFastRegen-API.patch
new file mode 100644
index 0000000000..47c59d0fd5
--- /dev/null
+++ b/patches/server/0090-EntityRegainHealthEvent-isFastRegen-API.patch
@@ -0,0 +1,42 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Fri, 22 Apr 2016 01:43:11 -0500
+Subject: [PATCH] EntityRegainHealthEvent isFastRegen API
+
+Don't even get me started
+
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index de88eec263fd327c3d72e69bcd1c32343379d33e..4938b2caf5f8a32f6076f89fd2119fb965539e36 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -1350,10 +1350,16 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ public void heal(float f, EntityRegainHealthEvent.RegainReason regainReason) {
++ // Paper start - Forward
++ heal(f, regainReason, false);
++ }
++
++ public void heal(float f, EntityRegainHealthEvent.RegainReason regainReason, boolean isFastRegen) {
++ // Paper end
+ float f1 = this.getHealth();
+
+ if (f1 > 0.0F) {
+- EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), f, regainReason);
++ EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), f, regainReason, isFastRegen); // Paper
+ // Suppress during worldgen
+ if (this.valid) {
+ this.level().getCraftServer().getPluginManager().callEvent(event);
+diff --git a/src/main/java/net/minecraft/world/food/FoodData.java b/src/main/java/net/minecraft/world/food/FoodData.java
+index 85cad5c83a1e5f271eb33f8b4f61cee2cbd3481e..6a686be6a69ae890d519a54ca099d4ba14e5b9e1 100644
+--- a/src/main/java/net/minecraft/world/food/FoodData.java
++++ b/src/main/java/net/minecraft/world/food/FoodData.java
+@@ -79,7 +79,7 @@ public class FoodData {
+ if (this.tickTimer >= this.saturatedRegenRate) { // CraftBukkit
+ float f = Math.min(this.saturationLevel, 6.0F);
+
+- player.heal(f / 6.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED); // CraftBukkit - added RegainReason
++ player.heal(f / 6.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED, true); // CraftBukkit - added RegainReason // Paper - This is fast regen
+ // this.addExhaustion(f); CraftBukkit - EntityExhaustionEvent
+ player.causeFoodExhaustion(f, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.REGEN); // CraftBukkit - EntityExhaustionEvent
+ this.tickTimer = 0;
diff --git a/patches/server/0091-Add-ability-to-configure-frosted_ice-properties.patch b/patches/server/0091-Add-ability-to-configure-frosted_ice-properties.patch
new file mode 100644
index 0000000000..d8eb02d3f1
--- /dev/null
+++ b/patches/server/0091-Add-ability-to-configure-frosted_ice-properties.patch
@@ -0,0 +1,32 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kashike <[email protected]>
+Date: Thu, 21 Apr 2016 23:51:55 -0700
+Subject: [PATCH] Add ability to configure frosted_ice properties
+
+
+diff --git a/src/main/java/net/minecraft/world/level/block/FrostedIceBlock.java b/src/main/java/net/minecraft/world/level/block/FrostedIceBlock.java
+index 2620d4c4f42578f6b4a3cc8142d55ca3756d7aa0..56e5a6351d5fa99167ba8a544812e3619c13b953 100644
+--- a/src/main/java/net/minecraft/world/level/block/FrostedIceBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/FrostedIceBlock.java
+@@ -42,6 +42,7 @@ public class FrostedIceBlock extends IceBlock {
+
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
++ if (!world.paperConfig().environment.frostedIce.enabled) return; // Paper - Frosted ice options
+ if ((random.nextInt(3) == 0 || this.fewerNeigboursThan(world, pos, 4))
+ && world.getMaxLocalRawBrightness(pos) > 11 - state.getValue(AGE) - state.getLightBlock()
+ && this.slightlyMelt(state, world, pos)) {
+@@ -51,11 +52,11 @@ public class FrostedIceBlock extends IceBlock {
+ mutableBlockPos.setWithOffset(pos, direction);
+ BlockState blockState = world.getBlockState(mutableBlockPos);
+ if (blockState.is(this) && !this.slightlyMelt(blockState, world, mutableBlockPos)) {
+- world.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, 20, 40));
++ world.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, world.paperConfig().environment.frostedIce.delay.min, world.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
+ }
+ }
+ } else {
+- world.scheduleTick(pos, this, Mth.nextInt(random, 20, 40));
++ world.scheduleTick(pos, this, Mth.nextInt(random, world.paperConfig().environment.frostedIce.delay.min, world.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
+ }
+ }
+
diff --git a/patches/server/0092-remove-null-possibility-for-getServer-singleton.patch b/patches/server/0092-remove-null-possibility-for-getServer-singleton.patch
new file mode 100644
index 0000000000..63ff3b8ad4
--- /dev/null
+++ b/patches/server/0092-remove-null-possibility-for-getServer-singleton.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Thu, 28 Apr 2016 00:57:27 -0400
+Subject: [PATCH] remove null possibility for getServer singleton
+
+to stop IDE complaining about potential NPE
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index d374ff51987c30a84e137dd623e0f64966999b63..bf5f1cfc7f0c129083ada539d450b9c74d2d5d9c 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -203,6 +203,7 @@ import org.bukkit.event.server.ServerLoadEvent;
+
+ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
+
++ private static MinecraftServer SERVER; // Paper
+ public static final Logger LOGGER = LogUtils.getLogger();
+ public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper
+ public static final String VANILLA_BRAND = "vanilla";
+@@ -340,6 +341,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
+ super("Server");
++ SERVER = this; // Paper - better singleton
+ this.metricsRecorder = InactiveMetricsRecorder.INSTANCE;
+ this.onMetricsRecordingStopped = (methodprofilerresults) -> {
+ this.stopRecordingMetrics();
+@@ -2581,9 +2583,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ return false;
+ }
+
+- @Deprecated
+ public static MinecraftServer getServer() {
+- return (Bukkit.getServer() instanceof CraftServer) ? ((CraftServer) Bukkit.getServer()).getServer() : null;
++ return SERVER; // Paper
+ }
+
+ @Deprecated
diff --git a/patches/server/0093-Don-t-save-empty-scoreboard-teams-to-scoreboard.dat.patch b/patches/server/0093-Don-t-save-empty-scoreboard-teams-to-scoreboard.dat.patch
new file mode 100644
index 0000000000..e8fe46f840
--- /dev/null
+++ b/patches/server/0093-Don-t-save-empty-scoreboard-teams-to-scoreboard.dat.patch
@@ -0,0 +1,18 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sat, 7 May 2016 23:33:08 -0400
+Subject: [PATCH] Don't save empty scoreboard teams to scoreboard.dat
+
+
+diff --git a/src/main/java/net/minecraft/world/scores/ScoreboardSaveData.java b/src/main/java/net/minecraft/world/scores/ScoreboardSaveData.java
+index 8b7a84ff5ecac694166c96c2d2592b6073a0cfc0..618bbdfbb0f790ee9c55a8f720436c22dbc9775a 100644
+--- a/src/main/java/net/minecraft/world/scores/ScoreboardSaveData.java
++++ b/src/main/java/net/minecraft/world/scores/ScoreboardSaveData.java
+@@ -148,6 +148,7 @@ public class ScoreboardSaveData extends SavedData {
+ ListTag listTag = new ListTag();
+
+ for (PlayerTeam playerTeam : this.scoreboard.getPlayerTeams()) {
++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().scoreboards.saveEmptyScoreboardTeams && playerTeam.getPlayers().isEmpty()) continue; // Paper - Don't save empty scoreboard teams to scoreboard.dat
+ CompoundTag compoundTag = new CompoundTag();
+ compoundTag.putString("Name", playerTeam.getName());
+ compoundTag.putString("DisplayName", Component.Serializer.toJson(playerTeam.getDisplayName(), registries));
diff --git a/patches/server/0094-LootTable-API-and-replenishable-lootables.patch b/patches/server/0094-LootTable-API-and-replenishable-lootables.patch
new file mode 100644
index 0000000000..97e331b6e4
--- /dev/null
+++ b/patches/server/0094-LootTable-API-and-replenishable-lootables.patch
@@ -0,0 +1,964 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sun, 1 May 2016 21:19:14 -0400
+Subject: [PATCH] LootTable API and replenishable lootables
+
+Provides an API to control the loot table for an object.
+Also provides a feature that any Lootable Inventory (Chests in Structures)
+can automatically replenish after a given time.
+
+This feature is good for long term worlds so that newer players
+do not suffer with "Every chest has been looted"
+
+== AT ==
+public org.bukkit.craftbukkit.block.CraftBlockEntityState getTileEntity()Lnet/minecraft/world/level/block/entity/BlockEntity;
+public org.bukkit.craftbukkit.block.CraftLootable setLootTable(Lorg/bukkit/loot/LootTable;J)V
+public org.bukkit.craftbukkit.entity.CraftMinecartContainer setLootTable(Lorg/bukkit/loot/LootTable;J)V
+
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a53d51be1da25b87f2bc0a29a196d8f9996dbd2b
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java
+@@ -0,0 +1,21 @@
++package com.destroystokyo.paper.loottable;
++
++import org.bukkit.loot.LootTable;
++import org.bukkit.loot.Lootable;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootable extends Lootable {
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table) {
++ this.setLootTable(table, this.getSeed());
++ }
++
++ @Override
++ default void setSeed(final long seed) {
++ this.setLootTable(this.getLootTable(), seed);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9e9ea13234703d3e4a39eed2b007e8be69dfbd12
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java
+@@ -0,0 +1,27 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.RandomizableContainer;
++import org.bukkit.craftbukkit.CraftLootTable;
++import org.bukkit.loot.LootTable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public interface PaperLootableBlock extends PaperLootable {
++
++ RandomizableContainer getRandomizableContainer();
++
++ /* Lootable */
++ @Override
++ default @Nullable LootTable getLootTable() {
++ return CraftLootTable.minecraftToBukkit(this.getRandomizableContainer().getLootTable());
++ }
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table, final long seed) {
++ this.getRandomizableContainer().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
++ }
++
++ @Override
++ default long getSeed() {
++ return this.getRandomizableContainer().getLootTableSeed();
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0699c60920333ea1fec04e3c94d952244d2abeae
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java
+@@ -0,0 +1,26 @@
++package com.destroystokyo.paper.loottable;
++
++import java.util.Objects;
++import net.minecraft.core.BlockPos;
++import org.bukkit.block.Block;
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableBlockInventory extends LootableBlockInventory, PaperLootableInventory, PaperLootableBlock {
++
++ /* PaperLootableInventory */
++ @Override
++ default PaperLootableInventoryData lootableDataForAPI() {
++ return Objects.requireNonNull(this.getRandomizableContainer().lootableData(), "Can only manage loot tables on tile entities with lootableData");
++ }
++
++ /* LootableBlockInventory */
++ @Override
++ default Block getBlock() {
++ final BlockPos position = this.getRandomizableContainer().getBlockPos();
++ return CraftBlock.at(this.getNMSWorld(), position);
++ }
++
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d933054535c83f877888cd36cd8bd8bf9d93a9df
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java
+@@ -0,0 +1,29 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.entity.vehicle.ContainerEntity;
++import org.bukkit.craftbukkit.CraftLootTable;
++import org.bukkit.loot.LootTable;
++import org.bukkit.loot.Lootable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public interface PaperLootableEntity extends Lootable {
++
++ ContainerEntity getHandle();
++
++ /* Lootable */
++ @Override
++ default @Nullable LootTable getLootTable() {
++ return CraftLootTable.minecraftToBukkit(this.getHandle().getContainerLootTable());
++ }
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table, final long seed) {
++ this.getHandle().setContainerLootTable(CraftLootTable.bukkitToMinecraft(table));
++ this.getHandle().setContainerLootTableSeed(seed);
++ }
++
++ @Override
++ default long getSeed() {
++ return this.getHandle().getContainerLootTableSeed();
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5c57acc95f638a8bcb351ae44e9434a056835470
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
+@@ -0,0 +1,26 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.level.Level;
++import org.bukkit.entity.Entity;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableEntityInventory extends LootableEntityInventory, PaperLootableInventory, PaperLootableEntity {
++
++ /* PaperLootableInventory */
++ @Override
++ default Level getNMSWorld() {
++ return this.getHandle().level();
++ }
++
++ @Override
++ default PaperLootableInventoryData lootableDataForAPI() {
++ return this.getHandle().lootableData();
++ }
++
++ /* LootableEntityInventory */
++ default Entity getEntity() {
++ return ((net.minecraft.world.entity.Entity) this.getHandle()).getBukkitEntity();
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9e7c22ef49f1699df298f7121d50d27b4cb0923f
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
+@@ -0,0 +1,79 @@
++package com.destroystokyo.paper.loottable;
++
++import java.util.UUID;
++import net.minecraft.world.level.Level;
++import org.bukkit.World;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableInventory extends PaperLootable, LootableInventory {
++
++ /* impl */
++ PaperLootableInventoryData lootableDataForAPI();
++
++ Level getNMSWorld();
++
++ default World getBukkitWorld() {
++ return this.getNMSWorld().getWorld();
++ }
++
++ /* LootableInventory */
++ @Override
++ default boolean isRefillEnabled() {
++ return this.getNMSWorld().paperConfig().lootables.autoReplenish;
++ }
++
++ @Override
++ default boolean hasBeenFilled() {
++ return this.getLastFilled() != -1;
++ }
++
++ @Override
++ default boolean hasPlayerLooted(final UUID player) {
++ return this.lootableDataForAPI().hasPlayerLooted(player);
++ }
++
++ @Override
++ default boolean canPlayerLoot(final UUID player) {
++ return this.lootableDataForAPI().canPlayerLoot(player, this.getNMSWorld().paperConfig());
++ }
++
++ @Override
++ default Long getLastLooted(final UUID player) {
++ return this.lootableDataForAPI().getLastLooted(player);
++ }
++
++ @Override
++ default boolean setHasPlayerLooted(final UUID player, final boolean looted) {
++ final boolean hasLooted = this.hasPlayerLooted(player);
++ if (hasLooted != looted) {
++ this.lootableDataForAPI().setPlayerLootedState(player, looted);
++ }
++ return hasLooted;
++ }
++
++ @Override
++ default boolean hasPendingRefill() {
++ final long nextRefill = this.lootableDataForAPI().getNextRefill();
++ return nextRefill != -1 && nextRefill > this.lootableDataForAPI().getLastFill();
++ }
++
++ @Override
++ default long getLastFilled() {
++ return this.lootableDataForAPI().getLastFill();
++ }
++
++ @Override
++ default long getNextRefill() {
++ return this.lootableDataForAPI().getNextRefill();
++ }
++
++ @Override
++ default long setNextRefill(long refillAt) {
++ if (refillAt < -1) {
++ refillAt = -1;
++ }
++ return this.lootableDataForAPI().setNextRefill(refillAt);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..861bff267cb397e13e8e1c79bd0776b130c6e5da
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
+@@ -0,0 +1,249 @@
++package com.destroystokyo.paper.loottable;
++
++import io.papermc.paper.configuration.WorldConfiguration;
++import io.papermc.paper.configuration.type.DurationOrDisabled;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.Objects;
++import java.util.Random;
++import java.util.UUID;
++import java.util.concurrent.TimeUnit;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.world.RandomizableContainer;
++import net.minecraft.world.entity.vehicle.ContainerEntity;
++import org.bukkit.entity.Player;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public class PaperLootableInventoryData {
++
++ private static final Random RANDOM = new Random();
++
++ private long lastFill = -1;
++ private long nextRefill = -1;
++ private int numRefills = 0;
++ private @Nullable Map<UUID, Long> lootedPlayers;
++
++ public long getLastFill() {
++ return this.lastFill;
++ }
++
++ long getNextRefill() {
++ return this.nextRefill;
++ }
++
++ long setNextRefill(final long nextRefill) {
++ final long prev = this.nextRefill;
++ this.nextRefill = nextRefill;
++ return prev;
++ }
++
++ public <T> boolean shouldReplenish(final T lootTableHolder, final LootTableInterface<T> holderInterface, final net.minecraft.world.entity.player.@Nullable Player player) {
++
++ // No Loot Table associated
++ if (!holderInterface.hasLootTable(lootTableHolder)) {
++ return false;
++ }
++
++ // ALWAYS process the first fill or if the feature is disabled
++ if (this.lastFill == -1 || !holderInterface.paperConfig(lootTableHolder).lootables.autoReplenish) {
++ return true;
++ }
++
++ // Only process refills when a player is set
++ if (player == null) {
++ return false;
++ }
++
++ // Chest is not scheduled for refill
++ if (this.nextRefill == -1) {
++ return false;
++ }
++
++ final WorldConfiguration paperConfig = holderInterface.paperConfig(lootTableHolder);
++
++ // Check if max refills has been hit
++ if (paperConfig.lootables.maxRefills != -1 && this.numRefills >= paperConfig.lootables.maxRefills) {
++ return false;
++ }
++
++ // Refill has not been reached
++ if (this.nextRefill > System.currentTimeMillis()) {
++ return false;
++ }
++
++
++ final Player bukkitPlayer = (Player) player.getBukkitEntity();
++ final LootableInventoryReplenishEvent event = new LootableInventoryReplenishEvent(bukkitPlayer, holderInterface.getInventoryForEvent(lootTableHolder));
++ event.setCancelled(!this.canPlayerLoot(player.getUUID(), paperConfig));
++ return event.callEvent();
++ }
++
++ public interface LootTableInterface<T> {
++
++ WorldConfiguration paperConfig(T holder);
++
++ void setSeed(T holder, long seed);
++
++ boolean hasLootTable(T holder);
++
++ LootableInventory getInventoryForEvent(T holder);
++ }
++
++ public static final LootTableInterface<RandomizableContainer> CONTAINER = new LootTableInterface<>() {
++ @Override
++ public WorldConfiguration paperConfig(final RandomizableContainer holder) {
++ return Objects.requireNonNull(holder.getLevel(), "Can only manager loot replenishment on block entities in a world").paperConfig();
++ }
++
++ @Override
++ public void setSeed(final RandomizableContainer holder, final long seed) {
++ holder.setLootTableSeed(seed);
++ }
++
++ @Override
++ public boolean hasLootTable(final RandomizableContainer holder) {
++ return holder.getLootTable() != null;
++ }
++
++ @Override
++ public LootableInventory getInventoryForEvent(final RandomizableContainer holder) {
++ return holder.getLootableInventory();
++ }
++ };
++
++ public static final LootTableInterface<ContainerEntity> ENTITY = new LootTableInterface<>() {
++ @Override
++ public WorldConfiguration paperConfig(final ContainerEntity holder) {
++ return holder.level().paperConfig();
++ }
++
++ @Override
++ public void setSeed(final ContainerEntity holder, final long seed) {
++ holder.setContainerLootTableSeed(seed);
++ }
++
++ @Override
++ public boolean hasLootTable(final ContainerEntity holder) {
++ return holder.getContainerLootTable() != null;
++ }
++
++ @Override
++ public LootableInventory getInventoryForEvent(final ContainerEntity holder) {
++ return holder.getLootableInventory();
++ }
++ };
++
++ public <T> boolean shouldClearLootTable(final T lootTableHolder, final LootTableInterface<T> holderInterface, final net.minecraft.world.entity.player.@Nullable Player player) {
++ this.lastFill = System.currentTimeMillis();
++ final WorldConfiguration paperConfig = holderInterface.paperConfig(lootTableHolder);
++ if (paperConfig.lootables.autoReplenish) {
++ final long min = paperConfig.lootables.refreshMin.seconds();
++ final long max = paperConfig.lootables.refreshMax.seconds();
++ this.nextRefill = this.lastFill + (min + RANDOM.nextLong(max - min + 1)) * 1000L;
++ this.numRefills++;
++ if (paperConfig.lootables.resetSeedOnFill) {
++ holderInterface.setSeed(lootTableHolder, 0);
++ }
++ if (player != null) { // This means that numRefills can be incremented without a player being in the lootedPlayers list - Seems to be EntityMinecartChest specific
++ this.setPlayerLootedState(player.getUUID(), true);
++ }
++ return false;
++ }
++ return true;
++ }
++
++ private static final String ROOT = "Paper.LootableData";
++ private static final String LAST_FILL = "lastFill";
++ private static final String NEXT_REFILL = "nextRefill";
++ private static final String NUM_REFILLS = "numRefills";
++ private static final String LOOTED_PLAYERS = "lootedPlayers";
++
++ public void loadNbt(final CompoundTag base) {
++ if (!base.contains(ROOT, Tag.TAG_COMPOUND)) {
++ return;
++ }
++ final CompoundTag comp = base.getCompound(ROOT);
++ if (comp.contains(LAST_FILL)) {
++ this.lastFill = comp.getLong(LAST_FILL);
++ }
++ if (comp.contains(NEXT_REFILL)) {
++ this.nextRefill = comp.getLong(NEXT_REFILL);
++ }
++
++ if (comp.contains(NUM_REFILLS)) {
++ this.numRefills = comp.getInt(NUM_REFILLS);
++ }
++ if (comp.contains(LOOTED_PLAYERS, Tag.TAG_LIST)) {
++ final ListTag list = comp.getList(LOOTED_PLAYERS, Tag.TAG_COMPOUND);
++ final int size = list.size();
++ if (size > 0) {
++ this.lootedPlayers = new HashMap<>(list.size());
++ }
++ for (int i = 0; i < size; i++) {
++ final CompoundTag cmp = list.getCompound(i);
++ this.lootedPlayers.put(cmp.getUUID("UUID"), cmp.getLong("Time"));
++ }
++ }
++ }
++
++ public void saveNbt(final CompoundTag base) {
++ final CompoundTag comp = new CompoundTag();
++ if (this.nextRefill != -1) {
++ comp.putLong(NEXT_REFILL, this.nextRefill);
++ }
++ if (this.lastFill != -1) {
++ comp.putLong(LAST_FILL, this.lastFill);
++ }
++ if (this.numRefills != 0) {
++ comp.putInt(NUM_REFILLS, this.numRefills);
++ }
++ if (this.lootedPlayers != null && !this.lootedPlayers.isEmpty()) {
++ final ListTag list = new ListTag();
++ for (final Map.Entry<UUID, Long> entry : this.lootedPlayers.entrySet()) {
++ final CompoundTag cmp = new CompoundTag();
++ cmp.putUUID("UUID", entry.getKey());
++ cmp.putLong("Time", entry.getValue());
++ list.add(cmp);
++ }
++ comp.put(LOOTED_PLAYERS, list);
++ }
++
++ if (!comp.isEmpty()) {
++ base.put(ROOT, comp);
++ }
++ }
++
++ void setPlayerLootedState(final UUID player, final boolean looted) {
++ if (looted && this.lootedPlayers == null) {
++ this.lootedPlayers = new HashMap<>();
++ }
++ if (looted) {
++ this.lootedPlayers.put(player, System.currentTimeMillis());
++ } else if (this.lootedPlayers != null) {
++ this.lootedPlayers.remove(player);
++ }
++ }
++
++ boolean canPlayerLoot(final UUID player, final WorldConfiguration worldConfiguration) {
++ final @Nullable Long lastLooted = this.getLastLooted(player);
++ if (!worldConfiguration.lootables.restrictPlayerReloot || lastLooted == null) return true;
++
++ final DurationOrDisabled restrictPlayerRelootTime = worldConfiguration.lootables.restrictPlayerRelootTime;
++ if (restrictPlayerRelootTime.value().isEmpty()) return false;
++
++ return TimeUnit.SECONDS.toMillis(restrictPlayerRelootTime.value().get().seconds()) + lastLooted < System.currentTimeMillis();
++ }
++
++ boolean hasPlayerLooted(final UUID player) {
++ return this.lootedPlayers != null && this.lootedPlayers.containsKey(player);
++ }
++
++ @Nullable Long getLastLooted(final UUID player) {
++ return this.lootedPlayers != null ? this.lootedPlayers.get(player) : null;
++ }
++}
+diff --git a/src/main/java/net/minecraft/world/RandomizableContainer.java b/src/main/java/net/minecraft/world/RandomizableContainer.java
+index 9715f1b63aeea39bde9258275f51e3e8508ca6e4..084935138b1484f3d96e99f4e5655a6c04931907 100644
+--- a/src/main/java/net/minecraft/world/RandomizableContainer.java
++++ b/src/main/java/net/minecraft/world/RandomizableContainer.java
+@@ -28,7 +28,7 @@ public interface RandomizableContainer extends Container {
+
+ void setLootTable(@Nullable ResourceKey<LootTable> lootTable);
+
+- default void setLootTable(ResourceKey<LootTable> lootTableId, long lootTableSeed) {
++ default void setLootTable(@Nullable ResourceKey<LootTable> lootTableId, long lootTableSeed) { // Paper - add nullable
+ this.setLootTable(lootTableId);
+ this.setLootTableSeed(lootTableSeed);
+ }
+@@ -51,13 +51,14 @@ public interface RandomizableContainer extends Container {
+ default boolean tryLoadLootTable(CompoundTag nbt) {
+ if (nbt.contains("LootTable", 8)) {
+ this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
++ if (this.lootableData() != null && this.getLootTable() != null) this.lootableData().loadNbt(nbt); // Paper - LootTable API
+ if (nbt.contains("LootTableSeed", 4)) {
+ this.setLootTableSeed(nbt.getLong("LootTableSeed"));
+ } else {
+ this.setLootTableSeed(0L);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ } else {
+ return false;
+ }
+@@ -69,26 +70,44 @@ public interface RandomizableContainer extends Container {
+ return false;
+ } else {
+ nbt.putString("LootTable", resourceKey.location().toString());
++ if (this.lootableData() != null) this.lootableData().saveNbt(nbt); // Paper - LootTable API
+ long l = this.getLootTableSeed();
+ if (l != 0L) {
+ nbt.putLong("LootTableSeed", l);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ }
+ }
+
+ default void unpackLootTable(@Nullable Player player) {
++ // Paper start - LootTable API
++ this.unpackLootTable(player, false);
++ }
++ default void unpackLootTable(@Nullable final Player player, final boolean forceClearLootTable) {
++ // Paper end - LootTable API
+ Level level = this.getLevel();
+ BlockPos blockPos = this.getBlockPos();
+ ResourceKey<LootTable> resourceKey = this.getLootTable();
+- if (resourceKey != null && level != null && level.getServer() != null) {
++ // Paper start - LootTable API
++ lootReplenish: if (resourceKey != null && level != null && level.getServer() != null) {
++ if (this.lootableData() != null && !this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ if (forceClearLootTable) {
++ this.setLootTable(null);
++ }
++ break lootReplenish;
++ }
++ // Paper end - LootTable API
+ LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(resourceKey);
+ if (player instanceof ServerPlayer) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, resourceKey);
+ }
+
+- this.setLootTable(null);
++ // Paper start - LootTable API
++ if (forceClearLootTable || this.lootableData() == null || this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ this.setLootTable(null);
++ }
++ // Paper end - LootTable API
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockPos));
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -97,4 +116,16 @@ public interface RandomizableContainer extends Container {
+ lootTable.fill(this, builder.create(LootContextParamSets.CHEST), this.getLootTableSeed());
+ }
+ }
++
++ // Paper start - LootTable API
++ @Nullable @org.jetbrains.annotations.Contract(pure = true)
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return null; // some containers don't really have a "replenish" ability like decorated pots
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ final org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(java.util.Objects.requireNonNull(this.getLevel(), "Cannot manage loot tables on block entities not in world"), this.getBlockPos());
++ return (com.destroystokyo.paper.loottable.PaperLootableInventory) block.getState(false);
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
+index 9c871c74ddc9983f6b4df27c7614f7224b682269..8033abfd77bcc20326b992a9d81e2faa9582fb83 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
+@@ -181,7 +181,7 @@ public abstract class AbstractChestBoat extends AbstractBoat implements HasCusto
+ @Nullable
+ @Override
+ public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
+- if (this.lootTable != null && player.isSpectator()) {
++ if (this.lootTable != null && player.isSpectator()) { // Paper - LootTable API (TODO spectators can open chests that aren't ready to be re-generated but this doesn't support that)
+ return null;
+ } else {
+ this.unpackLootTable(playerInventory.player);
+@@ -229,6 +229,14 @@ public abstract class AbstractChestBoat extends AbstractBoat implements HasCusto
+ this.level().gameEvent((Holder) GameEvent.CONTAINER_CLOSE, this.position(), GameEvent.Context.of((Entity) player));
+ }
+
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ // CraftBukkit start
+ public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
+ private int maxStack = MAX_STACK;
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+index a4be7b19b626957efdf2f2507121f0085ba1da50..d528e8e4aea266c495377365f01e314001eb1970 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+@@ -36,6 +36,14 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme
+ public ResourceKey<LootTable> lootTable;
+ public long lootTableSeed;
+
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ // CraftBukkit start
+ public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
+ private int maxStack = MAX_STACK;
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
+index beba927cffdeedcd68d8048708f5bf1a409ff965..874a44ab77248665c2db243764e8542bfc0d6514 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
+@@ -62,22 +62,26 @@ public interface ContainerEntity extends Container, MenuProvider {
+ default void addChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+ if (this.getContainerLootTable() != null) {
+ nbt.putString("LootTable", this.getContainerLootTable().location().toString());
++ this.lootableData().saveNbt(nbt); // Paper
+ if (this.getContainerLootTableSeed() != 0L) {
+ nbt.putLong("LootTableSeed", this.getContainerLootTableSeed());
+ }
+- } else {
+- ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+ this.clearItemStacks();
+ if (nbt.contains("LootTable", 8)) {
+ this.setContainerLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
++ // Paper start - LootTable API
++ if (this.getContainerLootTable() != null) {
++ this.lootableData().loadNbt(nbt);
++ }
++ // Paper end - LootTable API
+ this.setContainerLootTableSeed(nbt.getLong("LootTableSeed"));
+- } else {
+- ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void chestVehicleDestroyed(DamageSource source, ServerLevel world, Entity vehicle) {
+@@ -97,13 +101,18 @@ public interface ContainerEntity extends Container, MenuProvider {
+
+ default void unpackChestVehicleLootTable(@Nullable Player player) {
+ MinecraftServer minecraftServer = this.level().getServer();
+- if (this.getContainerLootTable() != null && minecraftServer != null) {
++ if (minecraftServer != null && this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { // Paper - LootTable API
+ LootTable lootTable = minecraftServer.reloadableRegistries().getLootTable(this.getContainerLootTable());
+ if (player != null) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getContainerLootTable());
+ }
+
+- this.setContainerLootTable(null);
++ // Paper start - LootTable API
++ if (this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) {
++ this.setContainerLootTable(null);
++ }
++ // Paper end - LootTable API
++
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)this.level()).withParameter(LootContextParams.ORIGIN, this.position());
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -173,4 +182,14 @@ public interface ContainerEntity extends Container, MenuProvider {
+ default boolean isChestVehicleStillValid(Player player) {
+ return !this.isRemoved() && player.canInteractWithEntity(this.getBoundingBox(), 4.0);
+ }
++
++ // Paper start - LootTable API
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ throw new UnsupportedOperationException("Implement this method");
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ return ((com.destroystokyo.paper.loottable.PaperLootableInventory) ((net.minecraft.world.entity.Entity) this).getBukkitEntity());
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
+index 63e41d3ed8844d6d41ff57b85779e190e57dc889..0712818e2d9205078bfc8846452ba31388840034 100644
+--- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
+@@ -137,7 +137,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ itemEntity.setDefaultPickUpDelay();
+ world.addFreshEntity(itemEntity);
+ } else {
+- shulkerBoxBlockEntity.unpackLootTable(player);
++ shulkerBoxBlockEntity.unpackLootTable(player, true); // Paper - force clear loot table so replenish data isn't persisted in the stack
+ }
+ }
+
+@@ -147,7 +147,15 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ @Override
+ protected List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
+ BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
++ Runnable reAdd = null; // Paper
+ if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) {
++ // Paper start - clear loot table if it was already used
++ if (shulkerBoxBlockEntity.lootableData().getLastFill() != -1 || !builder.getLevel().paperConfig().lootables.retainUnlootedShulkerBoxLootTableOnNonPlayerBreak) {
++ net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableResourceKey = shulkerBoxBlockEntity.getLootTable();
++ reAdd = () -> shulkerBoxBlockEntity.setLootTable(lootTableResourceKey);
++ shulkerBoxBlockEntity.setLootTable(null);
++ }
++ // Paper end
+ builder = builder.withDynamicDrop(CONTENTS, lootConsumer -> {
+ for (int i = 0; i < shulkerBoxBlockEntity.getContainerSize(); i++) {
+ lootConsumer.accept(shulkerBoxBlockEntity.getItem(i));
+@@ -155,7 +163,13 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ });
+ }
+
++ // Paper start - re-set loot table if it was cleared
++ try {
+ return super.getDrops(state, builder);
++ } finally {
++ if (reAdd != null) reAdd.run();
++ }
++ // Paper end - re-set loot table if it was cleared
+ }
+
+ @Override
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+index 74c833e589160f0fe31f3b5e515f3515201159bd..fc657b6052d4310ad9c28988042c2cf37cf5d213 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+@@ -115,4 +115,13 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
+ nbt.remove("LootTable");
+ nbt.remove("LootTableSeed");
+ }
++
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(); // Paper
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
+index 949e074a32b6593bd8b7405499e686a074e283e5..1f084b73f2ec67dd2022feafc5ab5dac02c338f6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
+@@ -58,7 +58,8 @@ public class CraftBrushableBlock extends CraftBlockEntityState<BrushableBlockEnt
+ this.setLootTable(this.getLootTable(), seed);
+ }
+
+- private void setLootTable(LootTable table, long seed) {
++ @Override // Paper - this is now an override
++ public void setLootTable(LootTable table, long seed) { // Paper - make public since it overrides a public method
+ this.getSnapshot().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+index 74315a46f6101775321b1cf4944c124c69aed182..f23fbb8ed39a754b36d2eb162358877ef6dacb17 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+@@ -8,7 +8,7 @@ import org.bukkit.craftbukkit.CraftLootTable;
+ import org.bukkit.loot.LootTable;
+ import org.bukkit.loot.Lootable;
+
+-public abstract class CraftLootable<T extends RandomizableContainerBlockEntity> extends CraftContainer<T> implements Nameable, Lootable {
++public abstract class CraftLootable<T extends RandomizableContainerBlockEntity> extends CraftContainer<T> implements Nameable, Lootable, com.destroystokyo.paper.loottable.PaperLootableBlockInventory { // Paper
+
+ public CraftLootable(World world, T tileEntity) {
+ super(world, tileEntity);
+@@ -27,29 +27,17 @@ public abstract class CraftLootable<T extends RandomizableContainerBlockEntity>
+ }
+ }
+
++ // Paper start - move to PaperLootableBlockInventory
+ @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getSnapshot().lootTable);
++ public net.minecraft.world.level.Level getNMSWorld() {
++ return ((org.bukkit.craftbukkit.CraftWorld) this.getWorld()).getHandle();
+ }
+
+ @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getSnapshot().lootTableSeed;
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- public void setLootTable(LootTable table, long seed) {
+- this.getSnapshot().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
++ public net.minecraft.world.RandomizableContainer getRandomizableContainer() {
++ return this.getSnapshot();
+ }
++ // Paper end - move to PaperLootableBlockInventory
+
+ @Override
+ public abstract CraftLootable<T> copy();
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
+index 62accb551344c41671fc22b15d7b25b6fc97d915..a1e04bb965f18ffd07e2f5bf827c5e4ddd6aeeda 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
+@@ -7,8 +7,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventory;
+ import org.bukkit.inventory.Inventory;
+ import org.bukkit.loot.LootTable;
+
+-public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat {
+-
++public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+ private final Inventory inventory;
+
+ public CraftChestBoat(CraftServer server, AbstractChestBoat entity) {
+@@ -31,28 +30,6 @@ public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.ent
+ return this.inventory;
+ }
+
+- @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
++ // Paper - moved loot table logic to PaperLootableEntityInventory
+
+- @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getHandle().getContainerLootTable());
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getHandle().getContainerLootTableSeed();
+- }
+-
+- private void setLootTable(LootTable table, long seed) {
+- this.getHandle().setContainerLootTable(CraftLootTable.bukkitToMinecraft(table));
+- this.getHandle().setContainerLootTableSeed(seed);
+- }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+index fd42f0b20132d08039ca7735d31a61806a6b07dc..b1a708de6790bbe336202b13ab862ced78de084f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+@@ -7,7 +7,7 @@ import org.bukkit.entity.minecart.StorageMinecart;
+ import org.bukkit.inventory.Inventory;
+
+ @SuppressWarnings("deprecation")
+-public class CraftMinecartChest extends CraftMinecartContainer implements StorageMinecart {
++public class CraftMinecartChest extends CraftMinecartContainer implements StorageMinecart, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+ private final CraftInventory inventory;
+
+ public CraftMinecartChest(CraftServer server, MinecartChest entity) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
+index 4388cd0303b45faf21631e7644baebb63baaba10..451f3a6f0b47493da3af3f5d6baced6a8c97f350 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
+@@ -7,7 +7,7 @@ import org.bukkit.craftbukkit.CraftServer;
+ import org.bukkit.loot.LootTable;
+ import org.bukkit.loot.Lootable;
+
+-public abstract class CraftMinecartContainer extends CraftMinecart implements Lootable {
++public abstract class CraftMinecartContainer extends CraftMinecart implements com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+
+ public CraftMinecartContainer(CraftServer server, AbstractMinecart entity) {
+ super(server, entity);
+@@ -18,27 +18,5 @@ public abstract class CraftMinecartContainer extends CraftMinecart implements Lo
+ return (AbstractMinecartContainer) this.entity;
+ }
+
+- @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
+-
+- @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getHandle().lootTable);
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getHandle().lootTableSeed;
+- }
+-
+- public void setLootTable(LootTable table, long seed) {
+- this.getHandle().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
+- }
++ // Paper - moved loot table logic to PaperLootableEntityInventory
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+index 39427b4f284e9402663be2b160ccb5f03f8b91da..17f5684cba9d3ed22d9925d1951520cc4751dfe2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+@@ -6,7 +6,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventory;
+ import org.bukkit.entity.minecart.HopperMinecart;
+ import org.bukkit.inventory.Inventory;
+
+-public final class CraftMinecartHopper extends CraftMinecartContainer implements HopperMinecart {
++public final class CraftMinecartHopper extends CraftMinecartContainer implements HopperMinecart, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+ private final CraftInventory inventory;
+
+ public CraftMinecartHopper(CraftServer server, MinecartHopper entity) {
diff --git a/patches/server/0095-System-property-for-disabling-watchdoge.patch b/patches/server/0095-System-property-for-disabling-watchdoge.patch
new file mode 100644
index 0000000000..4a35e21392
--- /dev/null
+++ b/patches/server/0095-System-property-for-disabling-watchdoge.patch
@@ -0,0 +1,19 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Thu, 12 May 2016 23:02:58 -0500
+Subject: [PATCH] System property for disabling watchdoge
+
+
+diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
+index 35c90aaee30980610d168c01cbe9abfe04331cb8..81ccbe2533e6fc5899d6c068e421e0d7f1351d34 100644
+--- a/src/main/java/org/spigotmc/WatchdogThread.java
++++ b/src/main/java/org/spigotmc/WatchdogThread.java
+@@ -61,7 +61,7 @@ public class WatchdogThread extends Thread
+ while ( !this.stopping )
+ {
+ //
+- if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.monotonicMillis() > this.lastTick + this.timeoutTime )
++ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.monotonicMillis() > this.lastTick + this.timeoutTime && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable
+ {
+ Logger log = Bukkit.getServer().getLogger();
+ log.log( Level.SEVERE, "------------------------------" );
diff --git a/patches/server/0096-Async-GameProfileCache-saving.patch b/patches/server/0096-Async-GameProfileCache-saving.patch
new file mode 100644
index 0000000000..76634a7718
--- /dev/null
+++ b/patches/server/0096-Async-GameProfileCache-saving.patch
@@ -0,0 +1,86 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Mon, 16 May 2016 20:47:41 -0400
+Subject: [PATCH] Async GameProfileCache saving
+
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index bf5f1cfc7f0c129083ada539d450b9c74d2d5d9c..0eb856a27fefb8d7283617498a31b05f2a736192 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -1011,7 +1011,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ } catch (java.lang.InterruptedException ignored) {} // Paper
+ if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
+ MinecraftServer.LOGGER.info("Saving usercache.json");
+- this.getProfileCache().save();
++ this.getProfileCache().save(false); // Paper - Perf: Async GameProfileCache saving
+ }
+ // Spigot end
+
+diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+index e2cb85c8f121837e8a19e003e1e757f431dfaf2b..5d9772a6c35e8f849e8879f510b2c586b148074c 100644
+--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -267,7 +267,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+ }
+
+ if (this.convertOldUsers()) {
+- this.getProfileCache().save();
++ this.getProfileCache().save(false); // Paper - Perf: Async GameProfileCache saving
+ }
+
+ if (!OldUsersConverter.serverReadyAfterUserconversion(this)) {
+diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java
+index a50b72ed08d16cdce19ff01512353412df2ee5ae..197e2ec9f1445a8184d0dde0e9b02b39e3302b91 100644
+--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java
++++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java
+@@ -117,7 +117,7 @@ public class GameProfileCache {
+ GameProfileCache.GameProfileInfo usercache_usercacheentry = new GameProfileCache.GameProfileInfo(profile, date);
+
+ this.safeAdd(usercache_usercacheentry);
+- if( !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly ) this.save(); // Spigot - skip saving if disabled
++ if( !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly ) this.save(true); // Spigot - skip saving if disabled // Paper - Perf: Async GameProfileCache saving
+ }
+
+ private long getNextOperation() {
+@@ -150,7 +150,7 @@ public class GameProfileCache {
+ }
+
+ if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled
+- this.save();
++ this.save(true); // Paper - Perf: Async GameProfileCache saving
+ }
+
+ return optional;
+@@ -262,7 +262,7 @@ public class GameProfileCache {
+ return list;
+ }
+
+- public void save() {
++ public void save(boolean asyncSave) { // Paper - Perf: Async GameProfileCache saving
+ JsonArray jsonarray = new JsonArray();
+ DateFormat dateformat = GameProfileCache.createDateFormat();
+
+@@ -270,6 +270,7 @@ public class GameProfileCache {
+ jsonarray.add(GameProfileCache.writeGameProfile(usercache_usercacheentry, dateformat));
+ });
+ String s = this.gson.toJson(jsonarray);
++ Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving
+
+ try {
+ BufferedWriter bufferedwriter = Files.newWriter(this.file, StandardCharsets.UTF_8);
+@@ -294,6 +295,14 @@ public class GameProfileCache {
+ } catch (IOException ioexception) {
+ ;
+ }
++ // Paper start - Perf: Async GameProfileCache saving
++ };
++ if (asyncSave) {
++ io.papermc.paper.util.MCUtil.scheduleAsyncTask(save);
++ } else {
++ save.run();
++ }
++ // Paper end - Perf: Async GameProfileCache saving
+
+ }
+
diff --git a/patches/server/0097-Optional-TNT-doesn-t-move-in-water.patch b/patches/server/0097-Optional-TNT-doesn-t-move-in-water.patch
new file mode 100644
index 0000000000..18deb91aa7
--- /dev/null
+++ b/patches/server/0097-Optional-TNT-doesn-t-move-in-water.patch
@@ -0,0 +1,50 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Zach Brown <[email protected]>
+Date: Sun, 22 May 2016 20:20:55 -0500
+Subject: [PATCH] Optional TNT doesn't move in water
+
+
+diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
+index 5bc84cc5ba4dca412dbc159b7a798f52d6f813dc..1d5de664af21013f68d59b326b1427bc632352de 100644
+--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
++++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
+@@ -135,6 +135,27 @@ public class PrimedTnt extends Entity implements TraceableEntity {
+ }
+ }
+
++ // Paper start - Option to prevent TNT from moving in water
++ if (!this.isRemoved() && this.wasTouchingWater && this.level().paperConfig().fixes.preventTntFromMovingInWater) {
++ /*
++ * Author: Jedediah Smith <[email protected]>
++ */
++ // Send position and velocity updates to nearby players on every tick while the TNT is in water.
++ // This does pretty well at keeping their clients in sync with the server.
++ net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId());
++ if (ete != null) {
++ net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this);
++ net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround);
++
++ ete.seenBy.stream()
++ .filter(viewer -> (viewer.getPlayer().getX() - this.getX()) * (viewer.getPlayer().getY() - this.getY()) * (viewer.getPlayer().getZ() - this.getZ()) < 16 * 16)
++ .forEach(viewer -> {
++ viewer.send(velocityPacket);
++ viewer.send(positionPacket);
++ });
++ }
++ }
++ // Paper end - Option to prevent TNT from moving in water
+ }
+
+ private void explode() {
+@@ -221,4 +242,11 @@ public class PrimedTnt extends Entity implements TraceableEntity {
+ public final boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
+ return false;
+ }
++
++ // Paper start - Option to prevent TNT from moving in water
++ @Override
++ public boolean isPushedByFluid() {
++ return !level().paperConfig().fixes.preventTntFromMovingInWater && super.isPushedByFluid();
++ }
++ // Paper end - Option to prevent TNT from moving in water
+ }
diff --git a/patches/server/0098-Faster-redstone-torch-rapid-clock-removal.patch b/patches/server/0098-Faster-redstone-torch-rapid-clock-removal.patch
new file mode 100644
index 0000000000..e3f3a52b49
--- /dev/null
+++ b/patches/server/0098-Faster-redstone-torch-rapid-clock-removal.patch
@@ -0,0 +1,68 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Martin Panzer <[email protected]>
+Date: Mon, 23 May 2016 12:12:37 +0200
+Subject: [PATCH] Faster redstone torch rapid clock removal
+
+Only resize the the redstone torch list once, since resizing arrays / lists is costly
+
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index fbc2830aab0bfa35ff071bbee84ce00da2d0e405..8891eab56b1bb2ed253fbced383e14a8c177966b 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -170,6 +170,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ private org.spigotmc.TickLimiter tileLimiter;
+ private int tileTickPosition;
+ public final Map<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
++ public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here
+
+ public CraftWorld getWorld() {
+ return this.world;
+diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
+index ca09910f87b69f6a4af7e3d9da4ad8f917acd66f..26319e8247e9dfa2068700d7fadc21ea5f715561 100644
+--- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
+@@ -28,7 +28,7 @@ public class RedstoneTorchBlock extends BaseTorchBlock {
+
+ public static final MapCodec<RedstoneTorchBlock> CODEC = simpleCodec(RedstoneTorchBlock::new);
+ public static final BooleanProperty LIT = BlockStateProperties.LIT;
+- private static final Map<BlockGetter, List<RedstoneTorchBlock.Toggle>> RECENT_TOGGLES = new WeakHashMap();
++ // Paper - Faster redstone torch rapid clock removal; Move the mapped list to World
+ public static final int RECENT_TOGGLE_TIMER = 60;
+ public static final int MAX_RECENT_TOGGLES = 8;
+ public static final int RESTART_DELAY = 160;
+@@ -81,11 +81,15 @@ public class RedstoneTorchBlock extends BaseTorchBlock {
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ boolean flag = this.hasNeighborSignal(world, pos, state);
+- List<RedstoneTorchBlock.Toggle> list = (List) RedstoneTorchBlock.RECENT_TOGGLES.get(world);
+-
+- while (list != null && !list.isEmpty() && world.getGameTime() - ((RedstoneTorchBlock.Toggle) list.get(0)).when > 60L) {
+- list.remove(0);
++ // Paper start - Faster redstone torch rapid clock removal
++ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = world.redstoneUpdateInfos;
++ if (redstoneUpdateInfos != null) {
++ RedstoneTorchBlock.Toggle curr;
++ while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) {
++ redstoneUpdateInfos.poll();
++ }
+ }
++ // Paper end - Faster redstone torch rapid clock removal
+
+ // CraftBukkit start
+ org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
+@@ -161,9 +165,12 @@ public class RedstoneTorchBlock extends BaseTorchBlock {
+ }
+
+ private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) {
+- List<RedstoneTorchBlock.Toggle> list = (List) RedstoneTorchBlock.RECENT_TOGGLES.computeIfAbsent(world, (iblockaccess) -> {
+- return Lists.newArrayList();
+- });
++ // Paper start - Faster redstone torch rapid clock removal
++ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = world.redstoneUpdateInfos;
++ if (list == null) {
++ list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>();
++ }
++ // Paper end - Faster redstone torch rapid clock removal
+
+ if (addNew) {
+ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime()));
diff --git a/patches/server/0099-Add-server-name-parameter.patch b/patches/server/0099-Add-server-name-parameter.patch
new file mode 100644
index 0000000000..8405ca923a
--- /dev/null
+++ b/patches/server/0099-Add-server-name-parameter.patch
@@ -0,0 +1,25 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Martin Panzer <[email protected]>
+Date: Sat, 28 May 2016 16:54:03 +0200
+Subject: [PATCH] Add server-name parameter
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
+index c8171cb14612857a5a6f7b000c1cdfb62a59836f..ffb40be63a01221d81060356e2a3fec17c1fa603 100644
+--- a/src/main/java/org/bukkit/craftbukkit/Main.java
++++ b/src/main/java/org/bukkit/craftbukkit/Main.java
+@@ -168,6 +168,14 @@ public class Main {
+ .defaultsTo(new File[] {})
+ .describedAs("Jar file");
+ // Paper end
++
++ // Paper start
++ acceptsAll(asList("server-name"), "Name of the server")
++ .withRequiredArg()
++ .ofType(String.class)
++ .defaultsTo("Unknown Server")
++ .describedAs("Name");
++ // Paper end
+ }
+ };
+
diff --git a/patches/server/0100-Fix-global-sound-handling.patch b/patches/server/0100-Fix-global-sound-handling.patch
new file mode 100644
index 0000000000..aa06ccf2c3
--- /dev/null
+++ b/patches/server/0100-Fix-global-sound-handling.patch
@@ -0,0 +1,101 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Tue, 31 May 2016 22:53:50 -0400
+Subject: [PATCH] Fix global sound handling
+
+* Only send global sounds to same world if limiting radius
+* respect global sound events gamerule
+
+Co-authored-by: Evan McCarthy <[email protected]>
+Co-authored-by: lexikiq <[email protected]>
+Co-authored-by: Aikar <[email protected]>
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 7611f58246960ec3fd0521ab5f1743a5b08d8241..9cb02b168384f597fce1251696e77a1e74f0d774 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -1311,7 +1311,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ @Override
+ public void levelEvent(@Nullable Player player, int eventId, BlockPos pos, int data) {
+- this.server.getPlayerList().broadcast(player, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), 64.0D, this.dimension(), new ClientboundLevelEventPacket(eventId, pos, data, false));
++ this.server.getPlayerList().broadcast(player, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), 64.0D, this.dimension(), new ClientboundLevelEventPacket(eventId, pos, data, false)); // Paper - diff on change (the 64.0 distance is used as defaults for sound ranges in spigot config for ender dragon, end portal and wither)
+ }
+
+ public int getLogicalHeight() {
+@@ -2133,6 +2133,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return this.serverLevelData.getGameRules();
+ }
+
++ // Paper start - respect global sound events gamerule
++ public List<net.minecraft.server.level.ServerPlayer> getPlayersForGlobalSoundGamerule() {
++ return this.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) ? ((ServerLevel) this).getServer().getPlayerList().players : ((ServerLevel) this).players();
++ }
++
++ public double getGlobalSoundRangeSquared(java.util.function.Function<org.spigotmc.SpigotWorldConfig, Integer> rangeFunction) {
++ final double range = rangeFunction.apply(this.spigotConfig);
++ return range <= 0 ? 64.0 * 64.0 : range * range; // 64 is taken from default in ServerLevel#levelEvent
++ }
++ // Paper end - respect global sound events gamerule
++
+ @Override
+ public CrashReportCategory fillReportDetails(CrashReport report) {
+ CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
+diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+index 97301b6643aa8c7d8ef0c5960188afa42665b4b4..e9246613702325375ac1e4cf03402839b3a93146 100644
+--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+@@ -666,11 +666,12 @@ public class EnderDragon extends Mob implements Enemy {
+ // CraftBukkit start - Use relative location for far away sounds
+ // worldserver.globalLevelEvent(1028, this.blockPosition(), 0);
+ int viewDistance = worldserver.getCraftServer().getViewDistance() * 16;
+- for (net.minecraft.server.level.ServerPlayer player : worldserver.getServer().getPlayerList().players) {
++ for (net.minecraft.server.level.ServerPlayer player : worldserver.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
+ double deltaX = this.getX() - player.getX();
+ double deltaZ = this.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+- if ( worldserver.spigotConfig.dragonDeathSoundRadius > 0 && distanceSquared > worldserver.spigotConfig.dragonDeathSoundRadius * worldserver.spigotConfig.dragonDeathSoundRadius ) continue; // Spigot
++ final double soundRadiusSquared = worldserver.getGlobalSoundRangeSquared(config -> config.dragonDeathSoundRadius); // Paper - respect global sound events gamerule
++ if ( !worldserver.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared ) continue; // Spigot // Paper - respect global sound events gamerule
+ if (distanceSquared > viewDistance * viewDistance) {
+ double deltaLength = Math.sqrt(distanceSquared);
+ double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
+diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
+index e3a9d008c48010bf29fca938cc0f2f71686c50ea..af721305a3b31f4aa9a36dfbc1cbe0cd278fa6ad 100644
+--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
+@@ -275,11 +275,12 @@ public class WitherBoss extends Monster implements RangedAttackMob {
+ // CraftBukkit start - Use relative location for far away sounds
+ // worldserver.globalLevelEvent(1023, new BlockPosition(this), 0);
+ int viewDistance = world.getCraftServer().getViewDistance() * 16;
+- for (ServerPlayer player : (List<ServerPlayer>) MinecraftServer.getServer().getPlayerList().players) {
++ for (ServerPlayer player : world.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
+ double deltaX = this.getX() - player.getX();
+ double deltaZ = this.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+- if ( world.spigotConfig.witherSpawnSoundRadius > 0 && distanceSquared > world.spigotConfig.witherSpawnSoundRadius * world.spigotConfig.witherSpawnSoundRadius ) continue; // Spigot
++ final double soundRadiusSquared = world.getGlobalSoundRangeSquared(config -> config.witherSpawnSoundRadius); // Paper - respect global sound events gamerule
++ if ( !world.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared ) continue; // Spigot // Paper - respect global sound events gamerule
+ if (distanceSquared > viewDistance * viewDistance) {
+ double deltaLength = Math.sqrt(distanceSquared);
+ double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
+diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
+index 04fce5cc4350df81c7ea103b74b845313dd6cc37..08cbf02bba3633a84cce90c202d13f2beb5b88a2 100644
+--- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java
++++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
+@@ -66,11 +66,13 @@ public class EnderEyeItem extends Item {
+ // world.globalLevelEvent(1038, blockposition1.offset(1, 0, 1), 0);
+ int viewDistance = world.getCraftServer().getViewDistance() * 16;
+ BlockPos soundPos = blockposition1.offset(1, 0, 1);
+- for (ServerPlayer player : world.getServer().getPlayerList().players) {
++ final net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) world; // Paper - respect global sound events gamerule - ensured by isClientSide check above
++ for (ServerPlayer player : serverLevel.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
+ double deltaX = soundPos.getX() - player.getX();
+ double deltaZ = soundPos.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+- if (world.spigotConfig.endPortalSoundRadius > 0 && distanceSquared > world.spigotConfig.endPortalSoundRadius * world.spigotConfig.endPortalSoundRadius) continue; // Spigot
++ final double soundRadiusSquared = serverLevel.getGlobalSoundRangeSquared(config -> config.endPortalSoundRadius); // Paper - respect global sound events gamerule
++ if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared) continue; // Spigot // Paper - respect global sound events gamerule
+ if (distanceSquared > viewDistance * viewDistance) {
+ double deltaLength = Math.sqrt(distanceSquared);
+ double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;