From b89e6a019568a6de6af3acd3ac8a9c5e4cf145c4 Mon Sep 17 00:00:00 2001 From: Nassim Jahnke Date: Thu, 4 Jul 2024 12:34:11 +0200 Subject: Readd more patches --- gradle.properties | 2 +- ...0991-Chunk-System-Starlight-from-Moonrise.patch | 14 +- ...InWorldBounds-and-getBlockState-for-inlin.patch | 2 +- ...aytracing-for-EntityLiving-hasLineOfSight.patch | 2 +- .../server/1000-Entity-Activation-Range-2.0.patch | 2 +- patches/server/1002-Anti-Xray.patch | 2 +- ...Alternate-Current-redstone-implementation.patch | 4 +- ...007-Optimize-Collision-to-not-load-chunks.patch | 109 ++ ...ize-GoalSelector-Goal.Flag-Set-operations.patch | 160 --- ...ize-GoalSelector-Goal.Flag-Set-operations.patch | 160 +++ patches/server/1008-Optimize-Hoppers.patch | 644 --------- .../1009-Entity-load-save-limit-per-chunk.patch | 81 -- patches/server/1009-Optimize-Hoppers.patch | 644 +++++++++ .../1010-Entity-load-save-limit-per-chunk.patch | 81 ++ .../server/1010-Optimize-Voxel-Shape-Merging.patch | 123 -- .../1011-Optimize-Bit-Operations-by-inlining.patch | 213 --- .../server/1011-Optimize-Voxel-Shape-Merging.patch | 123 ++ .../1012-Optimize-Bit-Operations-by-inlining.patch | 213 +++ .../server/1012-Remove-streams-from-hot-code.patch | 215 --- ...thfinder-Remove-Streams-Optimized-collect.patch | 133 -- .../server/1013-Remove-streams-from-hot-code.patch | 215 +++ ...e-implementation-for-blockstate-state-loo.patch | 343 ----- ...thfinder-Remove-Streams-Optimized-collect.patch | 133 ++ ...e-implementation-for-blockstate-state-loo.patch | 343 +++++ ...entity-type-tags-suggestions-in-selectors.patch | 156 --- ...entity-type-tags-suggestions-in-selectors.patch | 156 +++ ...Handle-Oversized-block-entities-in-chunks.patch | 64 - .../server/1017-API-for-checking-sent-chunks.patch | 61 - ...Handle-Oversized-block-entities-in-chunks.patch | 64 + .../server/1018-API-for-checking-sent-chunks.patch | 61 + patches/server/1018-Configurable-Sand-Duping.patch | 19 - .../1019-Improve-boat-collision-performance.patch | 69 + .../1019-Improve-performance-of-mass-crafts.patch | 138 -- patches/server/1020-Configurable-Sand-Duping.patch | 19 + patches/server/1020-Properly-resend-entities.patch | 232 ---- .../server/1021-Optimise-general-POI-access.patch | 1059 +++++++++++++++ .../server/1021-Registry-Modification-API.patch | 1421 -------------------- .../1022-Add-registry-entry-and-builders.patch | 483 ------- .../1022-Improve-performance-of-mass-crafts.patch | 138 ++ .../server/1023-Improved-Watchdog-Support.patch | 433 ------ patches/server/1023-Properly-resend-entities.patch | 232 ++++ .../1024-Optimise-random-block-ticking.patch | 456 +++++++ .../1024-Proxy-ItemStack-to-CraftItemStack.patch | 300 ----- ...C-view-accessible-directly-from-ItemStack.patch | 266 ---- .../server/1025-Registry-Modification-API.patch | 1421 ++++++++++++++++++++ .../1026-Add-registry-entry-and-builders.patch | 483 +++++++ .../1026-Fix-NPE-for-Jukebox-setRecord.patch | 20 - .../1027-Fix-CraftWorld-isChunkGenerated.patch | 44 - .../server/1027-Improved-Watchdog-Support.patch | 433 ++++++ ...8-Add-debug-for-chunk-system-unload-crash.patch | 57 - .../1028-Proxy-ItemStack-to-CraftItemStack.patch | 300 +++++ ...C-view-accessible-directly-from-ItemStack.patch | 266 ++++ .../1030-optimize-dirt-and-snow-spreading.patch | 78 ++ .../1031-Fix-NPE-for-Jukebox-setRecord.patch | 20 + .../1032-Fix-CraftWorld-isChunkGenerated.patch | 44 + ...3-Add-debug-for-chunk-system-unload-crash.patch | 57 + .../1001-Fix-World-isChunkGenerated-calls.patch | 243 ---- ...007-Optimize-Collision-to-not-load-chunks.patch | 109 -- .../1017-Improve-boat-collision-performance.patch | 68 - .../server/1018-Optimise-general-POI-access.patch | 1060 --------------- .../server/1020-Execute-chunk-tasks-mid-tick.patch | 176 --- .../1021-Optimise-random-block-ticking.patch | 456 ------- ...llision-checking-in-player-move-packet-ha.patch | 170 --- ...llision-checking-in-player-move-packet-ha.patch | 170 +++ .../1029-optimize-dirt-and-snow-spreading.patch | 78 -- .../server/1033-Actually-optimise-explosions.patch | 521 ------- .../server/1034-Actually-optimise-explosions.patch | 521 +++++++ 67 files changed, 8083 insertions(+), 8500 deletions(-) create mode 100644 patches/server/1007-Optimize-Collision-to-not-load-chunks.patch delete mode 100644 patches/server/1007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch create mode 100644 patches/server/1008-Optimize-GoalSelector-Goal.Flag-Set-operations.patch delete mode 100644 patches/server/1008-Optimize-Hoppers.patch delete mode 100644 patches/server/1009-Entity-load-save-limit-per-chunk.patch create mode 100644 patches/server/1009-Optimize-Hoppers.patch create mode 100644 patches/server/1010-Entity-load-save-limit-per-chunk.patch delete mode 100644 patches/server/1010-Optimize-Voxel-Shape-Merging.patch delete mode 100644 patches/server/1011-Optimize-Bit-Operations-by-inlining.patch create mode 100644 patches/server/1011-Optimize-Voxel-Shape-Merging.patch create mode 100644 patches/server/1012-Optimize-Bit-Operations-by-inlining.patch delete mode 100644 patches/server/1012-Remove-streams-from-hot-code.patch delete mode 100644 patches/server/1013-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch create mode 100644 patches/server/1013-Remove-streams-from-hot-code.patch delete mode 100644 patches/server/1014-Custom-table-implementation-for-blockstate-state-loo.patch create mode 100644 patches/server/1014-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch create mode 100644 patches/server/1015-Custom-table-implementation-for-blockstate-state-loo.patch delete mode 100644 patches/server/1015-Fix-entity-type-tags-suggestions-in-selectors.patch create mode 100644 patches/server/1016-Fix-entity-type-tags-suggestions-in-selectors.patch delete mode 100644 patches/server/1016-Handle-Oversized-block-entities-in-chunks.patch delete mode 100644 patches/server/1017-API-for-checking-sent-chunks.patch create mode 100644 patches/server/1017-Handle-Oversized-block-entities-in-chunks.patch create mode 100644 patches/server/1018-API-for-checking-sent-chunks.patch delete mode 100644 patches/server/1018-Configurable-Sand-Duping.patch create mode 100644 patches/server/1019-Improve-boat-collision-performance.patch delete mode 100644 patches/server/1019-Improve-performance-of-mass-crafts.patch create mode 100644 patches/server/1020-Configurable-Sand-Duping.patch delete mode 100644 patches/server/1020-Properly-resend-entities.patch create mode 100644 patches/server/1021-Optimise-general-POI-access.patch delete mode 100644 patches/server/1021-Registry-Modification-API.patch delete mode 100644 patches/server/1022-Add-registry-entry-and-builders.patch create mode 100644 patches/server/1022-Improve-performance-of-mass-crafts.patch delete mode 100644 patches/server/1023-Improved-Watchdog-Support.patch create mode 100644 patches/server/1023-Properly-resend-entities.patch create mode 100644 patches/server/1024-Optimise-random-block-ticking.patch delete mode 100644 patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch delete mode 100644 patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch create mode 100644 patches/server/1025-Registry-Modification-API.patch create mode 100644 patches/server/1026-Add-registry-entry-and-builders.patch delete mode 100644 patches/server/1026-Fix-NPE-for-Jukebox-setRecord.patch delete mode 100644 patches/server/1027-Fix-CraftWorld-isChunkGenerated.patch create mode 100644 patches/server/1027-Improved-Watchdog-Support.patch delete mode 100644 patches/server/1028-Add-debug-for-chunk-system-unload-crash.patch create mode 100644 patches/server/1028-Proxy-ItemStack-to-CraftItemStack.patch create mode 100644 patches/server/1029-Make-a-PDC-view-accessible-directly-from-ItemStack.patch create mode 100644 patches/server/1030-optimize-dirt-and-snow-spreading.patch create mode 100644 patches/server/1031-Fix-NPE-for-Jukebox-setRecord.patch create mode 100644 patches/server/1032-Fix-CraftWorld-isChunkGenerated.patch create mode 100644 patches/server/1033-Add-debug-for-chunk-system-unload-crash.patch delete mode 100644 patches/unapplied/server/1001-Fix-World-isChunkGenerated-calls.patch delete mode 100644 patches/unapplied/server/1007-Optimize-Collision-to-not-load-chunks.patch delete mode 100644 patches/unapplied/server/1017-Improve-boat-collision-performance.patch delete mode 100644 patches/unapplied/server/1018-Optimise-general-POI-access.patch delete mode 100644 patches/unapplied/server/1020-Execute-chunk-tasks-mid-tick.patch delete mode 100644 patches/unapplied/server/1021-Optimise-random-block-ticking.patch delete mode 100644 patches/unapplied/server/1026-Optimise-collision-checking-in-player-move-packet-ha.patch create mode 100644 patches/unapplied/server/1027-Optimise-collision-checking-in-player-move-packet-ha.patch delete mode 100644 patches/unapplied/server/1029-optimize-dirt-and-snow-spreading.patch delete mode 100644 patches/unapplied/server/1033-Actually-optimise-explosions.patch create mode 100644 patches/unapplied/server/1034-Actually-optimise-explosions.patch diff --git a/gradle.properties b/gradle.properties index 067fcc6c85..7aa001f7cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ version=1.21-R0.1-SNAPSHOT mcVersion=1.21 # Set to true while updating Minecraft version -updatingMinecraft=true +updatingMinecraft=false org.gradle.caching=true org.gradle.parallel=true diff --git a/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch b/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch index 4e0a067615..1fa4249c11 100644 --- a/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch +++ b/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch @@ -27387,7 +27387,7 @@ index bd20bea7f76a7307f1698fb2dfef37125032d166..70c2017400168d4fef3c14462798edcf if (shape.isEmpty()) { return true; diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index e27d3547d1e19c137e05e6b8d075127a8bafb237..613752e2df61fdd8010f8ad0cf25257580bfe32e 100644 +index e27d3547d1e19c137e05e6b8d075127a8bafb237..a022bbaa16054c56696a7a03e71ef042340d88ec 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -102,7 +102,7 @@ import org.bukkit.entity.SpawnCategory; @@ -27489,17 +27489,19 @@ index e27d3547d1e19c137e05e6b8d075127a8bafb237..613752e2df61fdd8010f8ad0cf252575 int tilesThisCycle = 0; var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll toRemove.add(null); // Paper - Fix MC-117075 -@@ -828,6 +888,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -828,6 +888,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { // Spigot end } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) { tickingblockentity.tick(); ++ // Paper start - rewrite chunk system + if ((++tickedEntities & 7) == 0) { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); + } ++ // Paper end - rewrite chunk system } } this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 -@@ -850,6 +913,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -850,6 +915,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Paper end - Prevent block entity and entity crashes } @@ -27507,7 +27509,7 @@ index e27d3547d1e19c137e05e6b8d075127a8bafb237..613752e2df61fdd8010f8ad0cf252575 } // Paper start - Option to prevent armor stands from doing entity lookups @Override -@@ -949,7 +1013,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -949,7 +1015,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } // Paper end - Perf: Optimize capturedTileEntities lookup // CraftBukkit end @@ -27516,7 +27518,7 @@ index e27d3547d1e19c137e05e6b8d075127a8bafb237..613752e2df61fdd8010f8ad0cf252575 } public void setBlockEntity(BlockEntity blockEntity) { -@@ -1039,28 +1103,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -1039,28 +1105,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { this.getProfiler().incrementCounter("getEntities"); @@ -27550,7 +27552,7 @@ index e27d3547d1e19c137e05e6b8d075127a8bafb237..613752e2df61fdd8010f8ad0cf252575 } @Override -@@ -1075,36 +1124,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -1075,36 +1126,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE); } diff --git a/patches/server/0994-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/patches/server/0994-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch index 5d0a72906e..05b7688a22 100644 --- a/patches/server/0994-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch +++ b/patches/server/0994-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch @@ -29,7 +29,7 @@ index 02367ef1371dde94ff6c4cd40bd32e800d6ccaaf..7b0fc7135bc107103dcaed6dc0707b18 this.x = x; this.y = y; diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 613752e2df61fdd8010f8ad0cf25257580bfe32e..fe14d02b7c3ed2b69717622f392bf077171343fd 100644 +index a022bbaa16054c56696a7a03e71ef042340d88ec..645bdb59d791016083fa14015d94ea2c0d4d2cf0 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -399,7 +399,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl diff --git a/patches/server/0996-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch b/patches/server/0996-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch index a68ae54072..46a4939bd4 100644 --- a/patches/server/0996-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch +++ b/patches/server/0996-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch @@ -62,7 +62,7 @@ index bb8e962e63c7a2d931f9bd7f7c002aa35cfa5fd3..0fa131a6c98adb498fc8d534e0e39647 default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) { // Paper start - Add predicate for blocks when raytracing diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index fe14d02b7c3ed2b69717622f392bf077171343fd..ea81c6e924091bf86ddbc001f9ecdb58c9c3e5b2 100644 +index 645bdb59d791016083fa14015d94ea2c0d4d2cf0..d8adcbb64040cbd24df884f1ef79d4dff7f93a43 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -391,10 +391,87 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl diff --git a/patches/server/1000-Entity-Activation-Range-2.0.patch b/patches/server/1000-Entity-Activation-Range-2.0.patch index 321482ab1f..154d3a86cd 100644 --- a/patches/server/1000-Entity-Activation-Range-2.0.patch +++ b/patches/server/1000-Entity-Activation-Range-2.0.patch @@ -340,7 +340,7 @@ index 0b7f52021441d633c37543e8ae485e81c292b747..d7f8464bf3eed0e42a5fc7f14a5b243d + } diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index ea81c6e924091bf86ddbc001f9ecdb58c9c3e5b2..c64bb7858345d93e4b2e65404ffdaffde8458c0f 100644 +index d8adcbb64040cbd24df884f1ef79d4dff7f93a43..0a03a735ccdfa81c0607d5516b93a15eec43f3aa 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -156,6 +156,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl diff --git a/patches/server/1002-Anti-Xray.patch b/patches/server/1002-Anti-Xray.patch index f4b57bf98b..ab9a10b557 100644 --- a/patches/server/1002-Anti-Xray.patch +++ b/patches/server/1002-Anti-Xray.patch @@ -1168,7 +1168,7 @@ index 9b1a6d8351fb473eec75a2fd08fb892b770e3586..0d0b07c9199be9ca0d5ac3feb1d44f14 } // Paper end - Send empty chunk diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index c64bb7858345d93e4b2e65404ffdaffde8458c0f..4994b964819600604cb123d4cf7ec27978796bb8 100644 +index 0a03a735ccdfa81c0607d5516b93a15eec43f3aa..6bbaf4a60190047a36e463c774630bdc1cf049aa 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -171,6 +171,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl diff --git a/patches/server/1004-Add-Alternate-Current-redstone-implementation.patch b/patches/server/1004-Add-Alternate-Current-redstone-implementation.patch index a0f84b27d6..261590166f 100644 --- a/patches/server/1004-Add-Alternate-Current-redstone-implementation.patch +++ b/patches/server/1004-Add-Alternate-Current-redstone-implementation.patch @@ -2035,10 +2035,10 @@ index 8daa490817367391cac5c9852e0755b280fb9054..2d97ca1f3c625c0206d35b785c57d958 EntityCallbacks() {} diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 4994b964819600604cb123d4cf7ec27978796bb8..06e9fc703b0ef722cc21a365203a561009047d23 100644 +index 6bbaf4a60190047a36e463c774630bdc1cf049aa..0ef67317a922f28202c84c7a1f81f3c534d2b838 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1579,4 +1579,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl +@@ -1581,4 +1581,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl } } // Paper end - notify observers even if grow failed diff --git a/patches/server/1007-Optimize-Collision-to-not-load-chunks.patch b/patches/server/1007-Optimize-Collision-to-not-load-chunks.patch new file mode 100644 index 0000000000..31fddd9a4b --- /dev/null +++ b/patches/server/1007-Optimize-Collision-to-not-load-chunks.patch @@ -0,0 +1,109 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Thu, 2 Apr 2020 02:37:57 -0400 +Subject: [PATCH] Optimize Collision to not load chunks + +The collision code takes an AABB and generates a cuboid of checks rather +than a cylinder, so at high velocity this can generate a lot of chunk checks. + +Treat an unloaded chunk as a collision for entities, and also for players if +the "prevent moving into unloaded chunks" setting is enabled. + +If that serting is not enabled, collisions will be ignored for players, since +movement will load only the chunk the player enters anyways and avoids loading +massive amounts of surrounding chunks due to large AABB lookups. + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0d0b07c9199be9ca0d5ac3feb1d44f149ba69283..14fb62dedabf2d858bea1877c2e28d1a7b6f5be4 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -865,6 +865,7 @@ public abstract class PlayerList { + Vec3 vec3d = dimensiontransition.pos(); + + entityplayer1.forceSetPositionRotation(vec3d.x, vec3d.y, vec3d.z, dimensiontransition.yRot(), dimensiontransition.xRot()); ++ worldserver.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper + // CraftBukkit end + if (dimensiontransition.missingRespawnBlock()) { + entityplayer1.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)); +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 5d7bc6470ab3818b0a189aab18ff26c0180e3912..f1fb4e830c6720d09b22056e3d0b9a08fe2bd472 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -250,6 +250,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Paper end - Share random for entities to make them more random + public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason + ++ public boolean collisionLoadChunks = false; // Paper + private CraftEntity bukkitEntity; + + public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper +diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java +index 1c10835b59aaefa3a65ff64f784620bdc54ddcdc..cd89623a44f02d7db77f0d0f87545cf80841f403 100644 +--- a/src/main/java/net/minecraft/world/level/BlockCollisions.java ++++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java +@@ -66,18 +66,37 @@ public class BlockCollisions extends AbstractIterator { + @Override + protected T computeNext() { + while (this.cursor.advance()) { +- int i = this.cursor.nextX(); +- int j = this.cursor.nextY(); +- int k = this.cursor.nextZ(); ++ int i = this.cursor.nextX(); final int x = i; // Paper - OBFHELPER ++ int j = this.cursor.nextY(); final int y = j; // Paper - OBFHELPER ++ int k = this.cursor.nextZ(); final int z = k; // Paper - OBFHELPER + int l = this.cursor.getNextType(); + if (l != 3) { +- BlockGetter blockGetter = this.getChunk(i, k); +- if (blockGetter != null) { +- this.pos.set(i, j, k); +- BlockState blockState = blockGetter.getBlockState(this.pos); +- if ((!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) +- && (l != 1 || blockState.hasLargeCollisionShape()) +- && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { ++ // Paper start - ensure we don't load chunks ++ // BlockGetter blockGetter = this.getChunk(i, k); ++ if (true) { ++ final @Nullable Entity source = this.context instanceof net.minecraft.world.phys.shapes.EntityCollisionContext entityContext ? entityContext.getEntity() : null; ++ boolean far = source != null && io.papermc.paper.util.MCUtil.distanceSq(source.getX(), y, source.getZ(), x, y, z) > 14; ++ this.pos.set(x, y, z); ++ BlockState blockState; ++ if (this.collisionGetter instanceof net.minecraft.server.level.WorldGenRegion) { ++ BlockGetter blockGetter = this.getChunk(x, z); ++ if (blockGetter == null) { ++ continue; ++ } ++ blockState = blockGetter.getBlockState(this.pos); ++ } else if ((!far && source instanceof net.minecraft.server.level.ServerPlayer) || (source != null && source.collisionLoadChunks)) { ++ blockState = this.collisionGetter.getBlockState(this.pos); ++ } else { ++ blockState = this.collisionGetter.getBlockStateIfLoaded(this.pos); ++ } ++ if (blockState == null) { ++ if (!(source instanceof net.minecraft.server.level.ServerPlayer) || source.level().paperConfig().chunks.preventMovingIntoUnloadedChunks) { ++ return this.resultProvider.apply(new BlockPos.MutableBlockPos(x, y, z), Shapes.create(far ? source.getBoundingBox() : new AABB(new BlockPos(x, y, z)))); ++ } ++ continue; ++ } ++ if (/*(!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) &&*/ (l != 1 || blockState.hasLargeCollisionShape()) && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { // Paper - onlySuffocatingBlocks is only true on the client, so we don't care about it here ++ // Paper end + VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); + if (voxelShape == Shapes.block()) { + if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { +diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java +index e57cb7fe53e915d24246e44c7f49971f5b2ab2cf..1ad0c976c6e2d6d31397dff850a9de7c16d16fba 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java ++++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java +@@ -44,11 +44,13 @@ public interface CollisionGetter extends BlockGetter { + } + + default boolean noCollision(@Nullable Entity entity, AABB box) { ++ try { if (entity != null) entity.collisionLoadChunks = true; // Paper + for (VoxelShape voxelShape : this.getBlockCollisions(entity, box)) { + if (!voxelShape.isEmpty()) { + return false; + } + } ++ } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper + + if (!this.getEntityCollisions(entity, box).isEmpty()) { + return false; diff --git a/patches/server/1007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/1007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch deleted file mode 100644 index 1f46712639..0000000000 --- a/patches/server/1007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch +++ /dev/null @@ -1,160 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 6 Apr 2020 17:53:29 -0700 -Subject: [PATCH] Optimize GoalSelector Goal.Flag Set operations - -Optimise the stream.anyMatch statement to move to a bitset -where we can replace the call with a single bitwise operation. - -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -index 16f9a98b8a939e5ca7e2dc04f87134a7ed66736b..dd423302b1baa64ef86ded87a29659342e28c142 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -@@ -4,7 +4,16 @@ import java.util.EnumSet; - import net.minecraft.util.Mth; - - public abstract class Goal { -- private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); -+ private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be. -+ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector -+ -+ // Paper start - remove streams from pathfindergoalselector; make sure types are not empty -+ public Goal() { -+ if (this.goalTypes.size() == 0) { -+ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); -+ } -+ } -+ // Paper end - remove streams from pathfindergoalselector - - public abstract boolean canUse(); - -@@ -30,8 +39,13 @@ public abstract class Goal { - } - - public void setFlags(EnumSet controls) { -- this.flags.clear(); -- this.flags.addAll(controls); -+ // Paper start - remove streams from pathfindergoalselector -+ this.goalTypes.clear(); -+ this.goalTypes.addAll(controls); -+ if (this.goalTypes.size() == 0) { -+ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); -+ } -+ // Paper end - remove streams from pathfindergoalselector - } - - @Override -@@ -39,8 +53,10 @@ public abstract class Goal { - return this.getClass().getSimpleName(); - } - -- public EnumSet getFlags() { -- return this.flags; -+ // Paper start - remove streams from pathfindergoalselector -+ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { -+ return this.goalTypes; -+ // Paper end - remove streams from pathfindergoalselector - } - - protected int adjustedTickDelay(int ticks) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -index 074ef807258139f818e30494126585262c2f33c0..74d4f653d5c7f1923c59019effd78337402f7025 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -@@ -25,7 +25,8 @@ public class GoalSelector { - private final Map lockedFlags = new EnumMap<>(Goal.Flag.class); - private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); - private final Supplier profiler; -- private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); -+ private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector -+ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector - private int curRate; - - public GoalSelector(Supplier profiler) { -@@ -65,18 +66,18 @@ public class GoalSelector { - this.availableGoals.removeIf(wrappedGoalx -> wrappedGoalx.getGoal() == goal); - } - -- private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet controls) { -- for (Goal.Flag flag : goal.getFlags()) { -- if (controls.contains(flag)) { -- return true; -- } -- } -- -- return false; -+ // Paper start -+ private static boolean goalContainsAnyFlags(WrappedGoal goal, com.destroystokyo.paper.util.set.OptimizedSmallEnumSet controls) { -+ return goal.getFlags().hasCommonElements(controls); - } - - private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map goalsByControl) { -- for (Goal.Flag flag : goal.getFlags()) { -+ long flagIterator = goal.getFlags().getBackingSet(); -+ int wrappedGoalSize = goal.getFlags().size(); -+ for (int i = 0; i < wrappedGoalSize; ++i) { -+ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; -+ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); -+ // Paper end - if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) { - return false; - } -@@ -90,7 +91,7 @@ public class GoalSelector { - profilerFiller.push("goalCleanup"); - - for (WrappedGoal wrappedGoal : this.availableGoals) { -- if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.disabledFlags) || !wrappedGoal.canContinueToUse())) { -+ if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams - wrappedGoal.stop(); - } - } -@@ -100,11 +101,14 @@ public class GoalSelector { - profilerFiller.push("goalUpdate"); - - for (WrappedGoal wrappedGoal2 : this.availableGoals) { -- if (!wrappedGoal2.isRunning() -- && !goalContainsAnyFlags(wrappedGoal2, this.disabledFlags) -- && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) -- && wrappedGoal2.canUse()) { -- for (Goal.Flag flag : wrappedGoal2.getFlags()) { -+ // Paper start -+ if (!wrappedGoal2.isRunning() && !goalContainsAnyFlags(wrappedGoal2, this.goalTypes) && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) && wrappedGoal2.canUse()) { -+ long flagIterator = wrappedGoal2.getFlags().getBackingSet(); -+ int wrappedGoalSize = wrappedGoal2.getFlags().size(); -+ for (int i = 0; i < wrappedGoalSize; ++i) { -+ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; -+ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); -+ // Paper end - WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL); - wrappedGoal3.stop(); - this.lockedFlags.put(flag, wrappedGoal2); -@@ -136,11 +140,11 @@ public class GoalSelector { - } - - public void disableControlFlag(Goal.Flag control) { -- this.disabledFlags.add(control); -+ this.goalTypes.add(control); // Paper - remove streams from pathfindergoalselector - } - - public void enableControlFlag(Goal.Flag control) { -- this.disabledFlags.remove(control); -+ this.goalTypes.remove(control); // Paper - remove streams from pathfindergoalselector - } - - public void setControlFlag(Goal.Flag control, boolean enabled) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -index b02d3deb550830245c8945ef17d3073ea930fdda..65ccdbaa5230c02d44a5959bca0f6fc30237a6fd 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -@@ -69,8 +69,10 @@ public class WrappedGoal extends Goal { - } - - @Override -- public EnumSet getFlags() { -+ // Paper start - remove streams from pathfindergoalselector -+ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { - return this.goal.getFlags(); -+ // Paper end - remove streams from pathfindergoalselector - } - - public boolean isRunning() { diff --git a/patches/server/1008-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/1008-Optimize-GoalSelector-Goal.Flag-Set-operations.patch new file mode 100644 index 0000000000..1f46712639 --- /dev/null +++ b/patches/server/1008-Optimize-GoalSelector-Goal.Flag-Set-operations.patch @@ -0,0 +1,160 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 6 Apr 2020 17:53:29 -0700 +Subject: [PATCH] Optimize GoalSelector Goal.Flag Set operations + +Optimise the stream.anyMatch statement to move to a bitset +where we can replace the call with a single bitwise operation. + +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java +index 16f9a98b8a939e5ca7e2dc04f87134a7ed66736b..dd423302b1baa64ef86ded87a29659342e28c142 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java +@@ -4,7 +4,16 @@ import java.util.EnumSet; + import net.minecraft.util.Mth; + + public abstract class Goal { +- private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); ++ private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be. ++ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector ++ ++ // Paper start - remove streams from pathfindergoalselector; make sure types are not empty ++ public Goal() { ++ if (this.goalTypes.size() == 0) { ++ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); ++ } ++ } ++ // Paper end - remove streams from pathfindergoalselector + + public abstract boolean canUse(); + +@@ -30,8 +39,13 @@ public abstract class Goal { + } + + public void setFlags(EnumSet controls) { +- this.flags.clear(); +- this.flags.addAll(controls); ++ // Paper start - remove streams from pathfindergoalselector ++ this.goalTypes.clear(); ++ this.goalTypes.addAll(controls); ++ if (this.goalTypes.size() == 0) { ++ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); ++ } ++ // Paper end - remove streams from pathfindergoalselector + } + + @Override +@@ -39,8 +53,10 @@ public abstract class Goal { + return this.getClass().getSimpleName(); + } + +- public EnumSet getFlags() { +- return this.flags; ++ // Paper start - remove streams from pathfindergoalselector ++ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { ++ return this.goalTypes; ++ // Paper end - remove streams from pathfindergoalselector + } + + protected int adjustedTickDelay(int ticks) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 074ef807258139f818e30494126585262c2f33c0..74d4f653d5c7f1923c59019effd78337402f7025 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -25,7 +25,8 @@ public class GoalSelector { + private final Map lockedFlags = new EnumMap<>(Goal.Flag.class); + private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); + private final Supplier profiler; +- private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); ++ private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector ++ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector + private int curRate; + + public GoalSelector(Supplier profiler) { +@@ -65,18 +66,18 @@ public class GoalSelector { + this.availableGoals.removeIf(wrappedGoalx -> wrappedGoalx.getGoal() == goal); + } + +- private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet controls) { +- for (Goal.Flag flag : goal.getFlags()) { +- if (controls.contains(flag)) { +- return true; +- } +- } +- +- return false; ++ // Paper start ++ private static boolean goalContainsAnyFlags(WrappedGoal goal, com.destroystokyo.paper.util.set.OptimizedSmallEnumSet controls) { ++ return goal.getFlags().hasCommonElements(controls); + } + + private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map goalsByControl) { +- for (Goal.Flag flag : goal.getFlags()) { ++ long flagIterator = goal.getFlags().getBackingSet(); ++ int wrappedGoalSize = goal.getFlags().size(); ++ for (int i = 0; i < wrappedGoalSize; ++i) { ++ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; ++ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); ++ // Paper end + if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) { + return false; + } +@@ -90,7 +91,7 @@ public class GoalSelector { + profilerFiller.push("goalCleanup"); + + for (WrappedGoal wrappedGoal : this.availableGoals) { +- if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.disabledFlags) || !wrappedGoal.canContinueToUse())) { ++ if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams + wrappedGoal.stop(); + } + } +@@ -100,11 +101,14 @@ public class GoalSelector { + profilerFiller.push("goalUpdate"); + + for (WrappedGoal wrappedGoal2 : this.availableGoals) { +- if (!wrappedGoal2.isRunning() +- && !goalContainsAnyFlags(wrappedGoal2, this.disabledFlags) +- && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) +- && wrappedGoal2.canUse()) { +- for (Goal.Flag flag : wrappedGoal2.getFlags()) { ++ // Paper start ++ if (!wrappedGoal2.isRunning() && !goalContainsAnyFlags(wrappedGoal2, this.goalTypes) && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) && wrappedGoal2.canUse()) { ++ long flagIterator = wrappedGoal2.getFlags().getBackingSet(); ++ int wrappedGoalSize = wrappedGoal2.getFlags().size(); ++ for (int i = 0; i < wrappedGoalSize; ++i) { ++ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; ++ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); ++ // Paper end + WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL); + wrappedGoal3.stop(); + this.lockedFlags.put(flag, wrappedGoal2); +@@ -136,11 +140,11 @@ public class GoalSelector { + } + + public void disableControlFlag(Goal.Flag control) { +- this.disabledFlags.add(control); ++ this.goalTypes.add(control); // Paper - remove streams from pathfindergoalselector + } + + public void enableControlFlag(Goal.Flag control) { +- this.disabledFlags.remove(control); ++ this.goalTypes.remove(control); // Paper - remove streams from pathfindergoalselector + } + + public void setControlFlag(Goal.Flag control, boolean enabled) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java +index b02d3deb550830245c8945ef17d3073ea930fdda..65ccdbaa5230c02d44a5959bca0f6fc30237a6fd 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java +@@ -69,8 +69,10 @@ public class WrappedGoal extends Goal { + } + + @Override +- public EnumSet getFlags() { ++ // Paper start - remove streams from pathfindergoalselector ++ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { + return this.goal.getFlags(); ++ // Paper end - remove streams from pathfindergoalselector + } + + public boolean isRunning() { diff --git a/patches/server/1008-Optimize-Hoppers.patch b/patches/server/1008-Optimize-Hoppers.patch deleted file mode 100644 index 500c718a87..0000000000 --- a/patches/server/1008-Optimize-Hoppers.patch +++ /dev/null @@ -1,644 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 27 Apr 2016 22:09:52 -0400 -Subject: [PATCH] Optimize Hoppers - -* Removes unnecessary extra calls to .update() that are very expensive -* Lots of itemstack cloning removed. Only clone if the item is actually moved -* Return true when a plugin cancels inventory move item event instead of false, as false causes pulls to cycle through all items. - However, pushes do not exhibit the same behavior, so this is not something plugins could of been relying on. -* Add option (Default on) to cooldown hoppers when they fail to move an item due to full inventory -* Skip subsequent InventoryMoveItemEvents if a plugin does not use the item after first event fire for an iteration by tracking changes to the event via an internal event implementation. -* Don't check for Entities with Inventories if the block above us is also occluding (not just Inventoried) -* Remove Streams from Item Suck In and restore restore 1.12 AABB checks which is simpler and no voxel allocations (was doing TWO Item Suck ins) - -diff --git a/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5c42823726e70ce6c9d0121d074315488e8b3f60 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java -@@ -0,0 +1,31 @@ -+package io.papermc.paper.event.inventory; -+ -+import org.bukkit.event.inventory.InventoryMoveItemEvent; -+import org.bukkit.inventory.Inventory; -+import org.bukkit.inventory.ItemStack; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.NotNull; -+ -+@DefaultQualifier(NonNull.class) -+public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent { -+ -+ public boolean calledSetItem; -+ public boolean calledGetItem; -+ -+ public PaperInventoryMoveItemEvent(final @NotNull Inventory sourceInventory, final @NotNull ItemStack itemStack, final @NotNull Inventory destinationInventory, final boolean didSourceInitiate) { -+ super(sourceInventory, itemStack, destinationInventory, didSourceInitiate); -+ } -+ -+ @Override -+ public ItemStack getItem() { -+ this.calledGetItem = true; -+ return super.getItem(); -+ } -+ -+ @Override -+ public void setItem(final ItemStack itemStack) { -+ super.setItem(itemStack); -+ this.calledSetItem = true; -+ } -+} -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 91cc3eb6db2875710064f6b31413ccc84af56bb2..17b0e570016504d1b7704bbfa9ff2e3233b45d09 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1722,6 +1722,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent -+ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers - - this.profiler.push(() -> { - String s = String.valueOf(worldserver); -diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java -index 86197725f0f2ac1e650297ae7a79907578e0e8f1..312b57b4ef340935f4335989ce1d6a4b8b61532c 100644 ---- a/src/main/java/net/minecraft/world/item/ItemStack.java -+++ b/src/main/java/net/minecraft/world/item/ItemStack.java -@@ -809,10 +809,16 @@ public final class ItemStack implements DataComponentHolder { - } - - public ItemStack copy() { -- if (this.isEmpty()) { -+ // Paper start - Perf: Optimize Hoppers -+ return this.copy(false); -+ } -+ -+ public ItemStack copy(boolean originalItem) { -+ if (!originalItem && this.isEmpty()) { -+ // Paper end - Perf: Optimize Hoppers - return ItemStack.EMPTY; - } else { -- ItemStack itemstack = new ItemStack(this.getItem(), this.count, this.components.copy()); -+ ItemStack itemstack = new ItemStack(originalItem ? this.item : this.getItem(), this.count, this.components.copy()); // Paper - Perf: Optimize Hoppers - - itemstack.setPopTime(this.getPopTime()); - return itemstack; -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 cd3b952a228c09077c2e74183a34ddb32811280b..c0563260277f9f4bd9ff08993b2efb4bca9a0c60 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 -@@ -38,6 +38,7 @@ import co.aikar.timings.MinecraftTimings; // Paper - import co.aikar.timings.Timing; // Paper - - public abstract class BlockEntity { -+ static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers - - public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper - // CraftBukkit start - data containers -@@ -216,6 +217,7 @@ public abstract class BlockEntity { - - public void setChanged() { - if (this.level != null) { -+ if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers - BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); - } - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 92086ca118d55ec49cefa5bf18977f8706e3e4b4..8310d132006043e93c612890514c4c7f3eb1c74d 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -156,6 +156,43 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - } - -+ // Paper start - Perf: Optimize Hoppers -+ private static final int HOPPER_EMPTY = 0; -+ private static final int HOPPER_HAS_ITEMS = 1; -+ private static final int HOPPER_IS_FULL = 2; -+ -+ private static int getFullState(final HopperBlockEntity tileEntity) { -+ tileEntity.unpackLootTable(null); -+ -+ final List hopperItems = tileEntity.getItems(); -+ -+ boolean empty = true; -+ boolean full = true; -+ -+ for (int i = 0, len = hopperItems.size(); i < len; ++i) { -+ final ItemStack stack = hopperItems.get(i); -+ if (stack.isEmpty()) { -+ full = false; -+ continue; -+ } -+ -+ if (!full) { -+ // can't be full -+ return HOPPER_HAS_ITEMS; -+ } -+ -+ empty = false; -+ -+ if (stack.getCount() != stack.getMaxStackSize()) { -+ // can't be full or empty -+ return HOPPER_HAS_ITEMS; -+ } -+ } -+ -+ return empty ? HOPPER_EMPTY : (full ? HOPPER_IS_FULL : HOPPER_HAS_ITEMS); -+ } -+ // Paper end - Perf: Optimize Hoppers -+ - private static boolean tryMoveItems(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleansupplier) { - if (world.isClientSide) { - return false; -@@ -163,11 +200,12 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (!blockEntity.isOnCooldown() && (Boolean) state.getValue(HopperBlock.ENABLED)) { - boolean flag = false; - -- if (!blockEntity.isEmpty()) { -+ final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers -+ if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers - flag = HopperBlockEntity.ejectItems(world, pos, blockEntity); - } - -- if (!blockEntity.inventoryFull()) { -+ if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers - flag |= booleansupplier.getAsBoolean(); - } - -@@ -198,6 +236,202 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - return false; - } - -+ // Paper start - Perf: Optimize Hoppers -+ private static boolean skipPullModeEventFire; -+ private static boolean skipPushModeEventFire; -+ public static boolean skipHopperEvents; -+ -+ private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { -+ skipPushModeEventFire = skipHopperEvents; -+ boolean foundItem = false; -+ for (int i = 0; i < hopper.getContainerSize(); ++i) { -+ final ItemStack item = hopper.getItem(i); -+ if (!item.isEmpty()) { -+ foundItem = true; -+ ItemStack origItemStack = item; -+ ItemStack movedItem = origItemStack; -+ -+ final int originalItemCount = origItemStack.getCount(); -+ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); -+ origItemStack.setCount(movedItemCount); -+ -+ // We only need to fire the event once to give protection plugins a chance to cancel this event -+ // Because nothing uses getItem, every event call should end up the same result. -+ if (!skipPushModeEventFire) { -+ movedItem = callPushMoveEvent(destination, movedItem, hopper); -+ if (movedItem == null) { // cancelled -+ origItemStack.setCount(originalItemCount); -+ return false; -+ } -+ } -+ -+ final ItemStack remainingItem = addItem(hopper, destination, movedItem, direction); -+ final int remainingItemCount = remainingItem.getCount(); -+ if (remainingItemCount != movedItemCount) { -+ origItemStack = origItemStack.copy(true); -+ origItemStack.setCount(originalItemCount); -+ if (!origItemStack.isEmpty()) { -+ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); -+ } -+ hopper.setItem(i, origItemStack); -+ destination.setChanged(); -+ return true; -+ } -+ origItemStack.setCount(originalItemCount); -+ } -+ } -+ if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown -+ hopper.setCooldown(level.spigotConfig.hopperTransfer); -+ } -+ return false; -+ } -+ -+ private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { -+ ItemStack movedItem = origItemStack; -+ final int originalItemCount = origItemStack.getCount(); -+ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); -+ container.setChanged(); // original logic always marks source inv as changed even if no move happens. -+ movedItem.setCount(movedItemCount); -+ -+ if (!skipPullModeEventFire) { -+ movedItem = callPullMoveEvent(hopper, container, movedItem); -+ if (movedItem == null) { // cancelled -+ origItemStack.setCount(originalItemCount); -+ // Drastically improve performance by returning true. -+ // No plugin could of relied on the behavior of false as the other call -+ // site for IMIE did not exhibit the same behavior -+ return true; -+ } -+ } -+ -+ final ItemStack remainingItem = addItem(container, hopper, movedItem, null); -+ final int remainingItemCount = remainingItem.getCount(); -+ if (remainingItemCount != movedItemCount) { -+ origItemStack = origItemStack.copy(true); -+ origItemStack.setCount(originalItemCount); -+ if (!origItemStack.isEmpty()) { -+ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); -+ } -+ -+ ignoreTileUpdates = true; -+ container.setItem(i, origItemStack); -+ ignoreTileUpdates = false; -+ container.setChanged(); -+ return true; -+ } -+ origItemStack.setCount(originalItemCount); -+ -+ if (level.paperConfig().hopper.cooldownWhenFull) { -+ cooldownHopper(hopper); -+ } -+ -+ return false; -+ } -+ -+ @Nullable -+ private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { -+ final Inventory destinationInventory = getInventory(iinventory); -+ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), -+ CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); -+ final boolean result = event.callEvent(); -+ if (!event.calledGetItem && !event.calledSetItem) { -+ skipPushModeEventFire = true; -+ } -+ if (!result) { -+ cooldownHopper(hopper); -+ return null; -+ } -+ -+ if (event.calledSetItem) { -+ return CraftItemStack.asNMSCopy(event.getItem()); -+ } else { -+ return itemstack; -+ } -+ } -+ -+ @Nullable -+ private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { -+ final Inventory sourceInventory = getInventory(container); -+ final Inventory destination = getInventory(hopper); -+ -+ // Mirror is safe as no plugins ever use this item -+ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); -+ final boolean result = event.callEvent(); -+ if (!event.calledGetItem && !event.calledSetItem) { -+ skipPullModeEventFire = true; -+ } -+ if (!result) { -+ cooldownHopper(hopper); -+ return null; -+ } -+ -+ if (event.calledSetItem) { -+ return CraftItemStack.asNMSCopy(event.getItem()); -+ } else { -+ return itemstack; -+ } -+ } -+ -+ private static Inventory getInventory(final Container container) { -+ final Inventory sourceInventory; -+ if (container instanceof CompoundContainer compoundContainer) { -+ // Have to special-case large chests as they work oddly -+ sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer); -+ } else if (container instanceof BlockEntity blockEntity) { -+ sourceInventory = blockEntity.getOwner(false).getInventory(); -+ } else if (container.getOwner() != null) { -+ sourceInventory = container.getOwner().getInventory(); -+ } else { -+ sourceInventory = new CraftInventory(container); -+ } -+ return sourceInventory; -+ } -+ -+ private static void cooldownHopper(final Hopper hopper) { -+ if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) { -+ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer); -+ } -+ } -+ -+ private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { -+ if (iinventory instanceof WorldlyContainer) { -+ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { -+ if (!test.test(iinventory.getItem(i), i)) { -+ return false; -+ } -+ } -+ } else { -+ int size = iinventory.getContainerSize(); -+ for (int i = 0; i < size; i++) { -+ if (!test.test(iinventory.getItem(i), i)) { -+ return false; -+ } -+ } -+ } -+ return true; -+ } -+ -+ private static boolean anyMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { -+ if (iinventory instanceof WorldlyContainer) { -+ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { -+ if (test.test(iinventory.getItem(i), i)) { -+ return true; -+ } -+ } -+ } else { -+ int size = iinventory.getContainerSize(); -+ for (int i = 0; i < size; i++) { -+ if (test.test(iinventory.getItem(i), i)) { -+ return true; -+ } -+ } -+ } -+ return true; -+ } -+ private static final java.util.function.BiPredicate STACK_SIZE_TEST = (itemstack, i) -> itemstack.getCount() >= itemstack.getMaxStackSize(); -+ private static final java.util.function.BiPredicate IS_EMPTY_TEST = (itemstack, i) -> itemstack.isEmpty(); -+ // Paper end - Perf: Optimize Hoppers -+ - private static boolean ejectItems(Level world, BlockPos pos, HopperBlockEntity blockEntity) { - Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity); - -@@ -209,47 +443,50 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (HopperBlockEntity.isFullContainer(iinventory, enumdirection)) { - return false; - } else { -- for (int i = 0; i < blockEntity.getContainerSize(); ++i) { -- ItemStack itemstack = blockEntity.getItem(i); -- -- if (!itemstack.isEmpty()) { -- int j = itemstack.getCount(); -- // CraftBukkit start - Call event when pushing items into other inventories -- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -- -- Inventory destinationInventory; -- // Have to special case large chests as they work oddly -- if (iinventory instanceof CompoundContainer) { -- destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -- } else if (iinventory.getOwner() != null) { -- destinationInventory = iinventory.getOwner().getInventory(); -- } else { -- destinationInventory = new CraftInventory(iinventory); -- } -- -- InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true); -- world.getCraftServer().getPluginManager().callEvent(event); -- if (event.isCancelled()) { -- blockEntity.setItem(i, itemstack); -- blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot -- return false; -- } -- ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); -- // CraftBukkit end -- -- if (itemstack1.isEmpty()) { -- iinventory.setChanged(); -- return true; -- } -- -- itemstack.setCount(j); -- if (j == 1) { -- blockEntity.setItem(i, itemstack); -- } -- } -- } -- -- return false; -+ // Paper start - Perf: Optimize Hoppers -+ return hopperPush(world, iinventory, enumdirection, blockEntity); -+ //for (int i = 0; i < blockEntity.getContainerSize(); ++i) { -+ // ItemStack itemstack = blockEntity.getItem(i); -+ -+ // if (!itemstack.isEmpty()) { -+ // int j = itemstack.getCount(); -+ // // CraftBukkit start - Call event when pushing items into other inventories -+ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -+ -+ // Inventory destinationInventory; -+ // // Have to special case large chests as they work oddly -+ // if (iinventory instanceof CompoundContainer) { -+ // destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -+ // } else if (iinventory.getOwner() != null) { -+ // destinationInventory = iinventory.getOwner().getInventory(); -+ // } else { -+ // destinationInventory = new CraftInventory(iinventory); -+ // } -+ -+ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentityhopper.getOwner().getInventory(), oitemstack, destinationInventory, true); -+ // world.getCraftServer().getPluginManager().callEvent(event); -+ // if (event.isCancelled()) { -+ // blockEntity.setItem(i, itemstack); -+ // blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot -+ // return false; -+ // } -+ // ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); -+ // // CraftBukkit end -+ -+ // if (itemstack1.isEmpty()) { -+ // iinventory.setChanged(); -+ // return true; -+ // } -+ -+ // itemstack.setCount(j); -+ // if (j == 1) { -+ // blockEntity.setItem(i, itemstack); -+ // } -+ // } -+ //} -+ -+ // return false; -+ // Paper end - Perf: Optimize Hoppers - } - } - } -@@ -300,7 +537,6 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - return false; - } - } -- - return true; - } - -@@ -311,6 +547,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - if (iinventory != null) { - Direction enumdirection = Direction.DOWN; -+ skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers - int[] aint = HopperBlockEntity.getSlots(iinventory, enumdirection); - int i = aint.length; - -@@ -346,44 +583,47 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - ItemStack itemstack = iinventory.getItem(i); - - if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) { -- int j = itemstack.getCount(); -- // CraftBukkit start - Call event on collection of items from inventories into the hopper -- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -- -- Inventory sourceInventory; -- // Have to special case large chests as they work oddly -- if (iinventory instanceof CompoundContainer) { -- sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -- } else if (iinventory.getOwner() != null) { -- sourceInventory = iinventory.getOwner().getInventory(); -- } else { -- sourceInventory = new CraftInventory(iinventory); -- } -- -- InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); -- -- Bukkit.getServer().getPluginManager().callEvent(event); -- if (event.isCancelled()) { -- iinventory.setItem(i, itemstack); -- -- if (ihopper instanceof HopperBlockEntity) { -- ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot -- } -- -- return false; -- } -- ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); -- // CraftBukkit end -- -- if (itemstack1.isEmpty()) { -- iinventory.setChanged(); -- return true; -- } -- -- itemstack.setCount(j); -- if (j == 1) { -- iinventory.setItem(i, itemstack); -- } -+ // Paper start - Perf: Optimize Hoppers -+ return hopperPull(world, ihopper, iinventory, itemstack, i); -+ // int j = itemstack.getCount(); -+ // // CraftBukkit start - Call event on collection of items from inventories into the hopper -+ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -+ -+ // Inventory sourceInventory; -+ // // Have to special case large chests as they work oddly -+ // if (iinventory instanceof CompoundContainer) { -+ // sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -+ // } else if (iinventory.getOwner() != null) { -+ // sourceInventory = iinventory.getOwner().getInventory(); -+ // } else { -+ // sourceInventory = new CraftInventory(iinventory); -+ // } -+ -+ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); -+ -+ // Bukkit.getServer().getPluginManager().callEvent(event); -+ // if (event.isCancelled()) { -+ // iinventory.setItem(i, itemstack); -+ -+ // if (ihopper instanceof HopperBlockEntity) { -+ // ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot -+ // } -+ -+ // return false; -+ // } -+ // ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); -+ // // CraftBukkit end -+ -+ // if (itemstack1.isEmpty()) { -+ // iinventory.setChanged(); -+ // return true; -+ // } -+ -+ // itemstack.setCount(j); -+ // if (j == 1) { -+ // iinventory.setItem(i, itemstack); -+ // } -+ // Paper end - Perf: Optimize Hoppers - } - - return false; -@@ -392,12 +632,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - public static boolean addItem(Container inventory, ItemEntity itemEntity) { - boolean flag = false; - // CraftBukkit start -- InventoryPickupItemEvent event = new InventoryPickupItemEvent(inventory.getOwner().getInventory(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); -+ if (InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers -+ InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation - itemEntity.level().getCraftServer().getPluginManager().callEvent(event); - if (event.isCancelled()) { - return false; - } - // CraftBukkit end -+ } // Paper - Perf: Optimize Hoppers - ItemStack itemstack = itemEntity.getItem().copy(); - ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null); - -@@ -491,7 +733,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - stack = stack.split(to.getMaxStackSize()); - } - // Spigot end -+ ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers - to.setItem(slot, stack); -+ ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers - stack = leftover; // Paper - Make hoppers respect inventory max stack size - flag = true; - } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { -@@ -571,14 +815,20 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - public static Container getContainerAt(Level world, BlockPos pos) { -- return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D); -+ return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true); - } - - @Nullable - private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z) { -+ // Paper start - Perf: Optimize Hoppers -+ return HopperBlockEntity.getContainerAt(world, pos, state, x, y, z, false); -+ } -+ @Nullable -+ private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z, boolean optimizeEntities) { -+ // Paper end - Perf: Optimize Hoppers - Container iinventory = HopperBlockEntity.getBlockContainer(world, pos, state); - -- if (iinventory == null) { -+ if (iinventory == null && (!optimizeEntities || !world.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers - iinventory = HopperBlockEntity.getEntityContainer(world, x, y, z); - } - -@@ -613,13 +863,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - private static Container getEntityContainer(Level world, double x, double y, double z) { -- List list = world.getEntities((Entity) null, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); -+ List list = world.getEntitiesOfClass((Class) Container.class, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper - Perf: Optimize hoppers - - return !list.isEmpty() ? (Container) list.get(world.random.nextInt(list.size())) : null; - } - - private static boolean canMergeItems(ItemStack first, ItemStack second) { -- return first.getCount() <= first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); -+ return first.getCount() < first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?! - } - - @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 13c9a68b604d4c7c6e09e72b3cea7ab2214b06ab..e2752752417c50b06f7c15b7d00bda0eaad3b0ae 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 -@@ -53,7 +53,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc - - @Override - public ItemStack getItem(int slot) { -- this.unpackLootTable(null); -+ if (slot == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers - return super.getItem(slot); - } - diff --git a/patches/server/1009-Entity-load-save-limit-per-chunk.patch b/patches/server/1009-Entity-load-save-limit-per-chunk.patch deleted file mode 100644 index 380beda077..0000000000 --- a/patches/server/1009-Entity-load-save-limit-per-chunk.patch +++ /dev/null @@ -1,81 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Wed, 18 Nov 2020 20:52:25 -0800 -Subject: [PATCH] Entity load/save limit per chunk - -Adds a config option to limit the number of entities saved and loaded -to a chunk. The default values of -1 disable the limit. Although -defaults are only included for certain entites, this allows setting -limits for any entity type. - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -index 997b05167c19472acb98edac32d4548cc65efa8e..87d2b3ec165e2e9e4bdbedd7adddaa2130ed507b 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -@@ -100,7 +100,18 @@ public final class ChunkEntitySlices { - } - - final ListTag entitiesTag = new ListTag(); -+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - for (final Entity entity : entities) { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ break; -+ } -+ savedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - CompoundTag compoundTag = new CompoundTag(); - if (entity.save(compoundTag)) { - entitiesTag.add(compoundTag); -diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java -index 842b0cec0397d7ae5166617627340ffac0e35db1..cb61462d4691a055a4b25f7b953609d8a154fdfe 100644 ---- a/src/main/java/net/minecraft/world/entity/EntityType.java -+++ b/src/main/java/net/minecraft/world/entity/EntityType.java -@@ -649,9 +649,20 @@ public class EntityType implements FeatureElement, EntityTypeT - final Spliterator spliterator = entityNbtList.spliterator(); - - return StreamSupport.stream(new Spliterator() { -+ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - public boolean tryAdvance(Consumer consumer) { - return spliterator.tryAdvance((nbtbase) -> { - EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, (entity) -> { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return null; -+ } -+ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - consumer.accept(entity); - return entity; - }); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -index 503ac0374e0c9f9993ad37bb8bd8cf1570d3615a..d4a505ef4af9ded02aeb1a817bcbe5b1a912a5b3 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -@@ -92,7 +92,18 @@ public class EntityStorage implements EntityPersistentStorage { - } - } else { - ListTag listTag = new ListTag(); -+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - dataList.getEntities().forEach(entity -> { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = this.level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return; -+ } -+ savedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - CompoundTag compoundTagx = new CompoundTag(); - if (entity.save(compoundTagx)) { - listTag.add(compoundTagx); diff --git a/patches/server/1009-Optimize-Hoppers.patch b/patches/server/1009-Optimize-Hoppers.patch new file mode 100644 index 0000000000..500c718a87 --- /dev/null +++ b/patches/server/1009-Optimize-Hoppers.patch @@ -0,0 +1,644 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 27 Apr 2016 22:09:52 -0400 +Subject: [PATCH] Optimize Hoppers + +* Removes unnecessary extra calls to .update() that are very expensive +* Lots of itemstack cloning removed. Only clone if the item is actually moved +* Return true when a plugin cancels inventory move item event instead of false, as false causes pulls to cycle through all items. + However, pushes do not exhibit the same behavior, so this is not something plugins could of been relying on. +* Add option (Default on) to cooldown hoppers when they fail to move an item due to full inventory +* Skip subsequent InventoryMoveItemEvents if a plugin does not use the item after first event fire for an iteration by tracking changes to the event via an internal event implementation. +* Don't check for Entities with Inventories if the block above us is also occluding (not just Inventoried) +* Remove Streams from Item Suck In and restore restore 1.12 AABB checks which is simpler and no voxel allocations (was doing TWO Item Suck ins) + +diff --git a/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5c42823726e70ce6c9d0121d074315488e8b3f60 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java +@@ -0,0 +1,31 @@ ++package io.papermc.paper.event.inventory; ++ ++import org.bukkit.event.inventory.InventoryMoveItemEvent; ++import org.bukkit.inventory.Inventory; ++import org.bukkit.inventory.ItemStack; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.NotNull; ++ ++@DefaultQualifier(NonNull.class) ++public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent { ++ ++ public boolean calledSetItem; ++ public boolean calledGetItem; ++ ++ public PaperInventoryMoveItemEvent(final @NotNull Inventory sourceInventory, final @NotNull ItemStack itemStack, final @NotNull Inventory destinationInventory, final boolean didSourceInitiate) { ++ super(sourceInventory, itemStack, destinationInventory, didSourceInitiate); ++ } ++ ++ @Override ++ public ItemStack getItem() { ++ this.calledGetItem = true; ++ return super.getItem(); ++ } ++ ++ @Override ++ public void setItem(final ItemStack itemStack) { ++ super.setItem(itemStack); ++ this.calledSetItem = true; ++ } ++} +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 91cc3eb6db2875710064f6b31413ccc84af56bb2..17b0e570016504d1b7704bbfa9ff2e3233b45d09 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1722,6 +1722,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent ++ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers + + this.profiler.push(() -> { + String s = String.valueOf(worldserver); +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 86197725f0f2ac1e650297ae7a79907578e0e8f1..312b57b4ef340935f4335989ce1d6a4b8b61532c 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -809,10 +809,16 @@ public final class ItemStack implements DataComponentHolder { + } + + public ItemStack copy() { +- if (this.isEmpty()) { ++ // Paper start - Perf: Optimize Hoppers ++ return this.copy(false); ++ } ++ ++ public ItemStack copy(boolean originalItem) { ++ if (!originalItem && this.isEmpty()) { ++ // Paper end - Perf: Optimize Hoppers + return ItemStack.EMPTY; + } else { +- ItemStack itemstack = new ItemStack(this.getItem(), this.count, this.components.copy()); ++ ItemStack itemstack = new ItemStack(originalItem ? this.item : this.getItem(), this.count, this.components.copy()); // Paper - Perf: Optimize Hoppers + + itemstack.setPopTime(this.getPopTime()); + return itemstack; +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 cd3b952a228c09077c2e74183a34ddb32811280b..c0563260277f9f4bd9ff08993b2efb4bca9a0c60 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 +@@ -38,6 +38,7 @@ import co.aikar.timings.MinecraftTimings; // Paper + import co.aikar.timings.Timing; // Paper + + public abstract class BlockEntity { ++ static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers + + public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper + // CraftBukkit start - data containers +@@ -216,6 +217,7 @@ public abstract class BlockEntity { + + public void setChanged() { + if (this.level != null) { ++ if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers + BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 92086ca118d55ec49cefa5bf18977f8706e3e4b4..8310d132006043e93c612890514c4c7f3eb1c74d 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -156,6 +156,43 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + } + ++ // Paper start - Perf: Optimize Hoppers ++ private static final int HOPPER_EMPTY = 0; ++ private static final int HOPPER_HAS_ITEMS = 1; ++ private static final int HOPPER_IS_FULL = 2; ++ ++ private static int getFullState(final HopperBlockEntity tileEntity) { ++ tileEntity.unpackLootTable(null); ++ ++ final List hopperItems = tileEntity.getItems(); ++ ++ boolean empty = true; ++ boolean full = true; ++ ++ for (int i = 0, len = hopperItems.size(); i < len; ++i) { ++ final ItemStack stack = hopperItems.get(i); ++ if (stack.isEmpty()) { ++ full = false; ++ continue; ++ } ++ ++ if (!full) { ++ // can't be full ++ return HOPPER_HAS_ITEMS; ++ } ++ ++ empty = false; ++ ++ if (stack.getCount() != stack.getMaxStackSize()) { ++ // can't be full or empty ++ return HOPPER_HAS_ITEMS; ++ } ++ } ++ ++ return empty ? HOPPER_EMPTY : (full ? HOPPER_IS_FULL : HOPPER_HAS_ITEMS); ++ } ++ // Paper end - Perf: Optimize Hoppers ++ + private static boolean tryMoveItems(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleansupplier) { + if (world.isClientSide) { + return false; +@@ -163,11 +200,12 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (!blockEntity.isOnCooldown() && (Boolean) state.getValue(HopperBlock.ENABLED)) { + boolean flag = false; + +- if (!blockEntity.isEmpty()) { ++ final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers ++ if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers + flag = HopperBlockEntity.ejectItems(world, pos, blockEntity); + } + +- if (!blockEntity.inventoryFull()) { ++ if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers + flag |= booleansupplier.getAsBoolean(); + } + +@@ -198,6 +236,202 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + return false; + } + ++ // Paper start - Perf: Optimize Hoppers ++ private static boolean skipPullModeEventFire; ++ private static boolean skipPushModeEventFire; ++ public static boolean skipHopperEvents; ++ ++ private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { ++ skipPushModeEventFire = skipHopperEvents; ++ boolean foundItem = false; ++ for (int i = 0; i < hopper.getContainerSize(); ++i) { ++ final ItemStack item = hopper.getItem(i); ++ if (!item.isEmpty()) { ++ foundItem = true; ++ ItemStack origItemStack = item; ++ ItemStack movedItem = origItemStack; ++ ++ final int originalItemCount = origItemStack.getCount(); ++ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); ++ origItemStack.setCount(movedItemCount); ++ ++ // We only need to fire the event once to give protection plugins a chance to cancel this event ++ // Because nothing uses getItem, every event call should end up the same result. ++ if (!skipPushModeEventFire) { ++ movedItem = callPushMoveEvent(destination, movedItem, hopper); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ return false; ++ } ++ } ++ ++ final ItemStack remainingItem = addItem(hopper, destination, movedItem, direction); ++ final int remainingItemCount = remainingItem.getCount(); ++ if (remainingItemCount != movedItemCount) { ++ origItemStack = origItemStack.copy(true); ++ origItemStack.setCount(originalItemCount); ++ if (!origItemStack.isEmpty()) { ++ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ } ++ hopper.setItem(i, origItemStack); ++ destination.setChanged(); ++ return true; ++ } ++ origItemStack.setCount(originalItemCount); ++ } ++ } ++ if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown ++ hopper.setCooldown(level.spigotConfig.hopperTransfer); ++ } ++ return false; ++ } ++ ++ private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { ++ ItemStack movedItem = origItemStack; ++ final int originalItemCount = origItemStack.getCount(); ++ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); ++ container.setChanged(); // original logic always marks source inv as changed even if no move happens. ++ movedItem.setCount(movedItemCount); ++ ++ if (!skipPullModeEventFire) { ++ movedItem = callPullMoveEvent(hopper, container, movedItem); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ // Drastically improve performance by returning true. ++ // No plugin could of relied on the behavior of false as the other call ++ // site for IMIE did not exhibit the same behavior ++ return true; ++ } ++ } ++ ++ final ItemStack remainingItem = addItem(container, hopper, movedItem, null); ++ final int remainingItemCount = remainingItem.getCount(); ++ if (remainingItemCount != movedItemCount) { ++ origItemStack = origItemStack.copy(true); ++ origItemStack.setCount(originalItemCount); ++ if (!origItemStack.isEmpty()) { ++ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ } ++ ++ ignoreTileUpdates = true; ++ container.setItem(i, origItemStack); ++ ignoreTileUpdates = false; ++ container.setChanged(); ++ return true; ++ } ++ origItemStack.setCount(originalItemCount); ++ ++ if (level.paperConfig().hopper.cooldownWhenFull) { ++ cooldownHopper(hopper); ++ } ++ ++ return false; ++ } ++ ++ @Nullable ++ private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { ++ final Inventory destinationInventory = getInventory(iinventory); ++ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), ++ CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); ++ final boolean result = event.callEvent(); ++ if (!event.calledGetItem && !event.calledSetItem) { ++ skipPushModeEventFire = true; ++ } ++ if (!result) { ++ cooldownHopper(hopper); ++ return null; ++ } ++ ++ if (event.calledSetItem) { ++ return CraftItemStack.asNMSCopy(event.getItem()); ++ } else { ++ return itemstack; ++ } ++ } ++ ++ @Nullable ++ private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { ++ final Inventory sourceInventory = getInventory(container); ++ final Inventory destination = getInventory(hopper); ++ ++ // Mirror is safe as no plugins ever use this item ++ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); ++ final boolean result = event.callEvent(); ++ if (!event.calledGetItem && !event.calledSetItem) { ++ skipPullModeEventFire = true; ++ } ++ if (!result) { ++ cooldownHopper(hopper); ++ return null; ++ } ++ ++ if (event.calledSetItem) { ++ return CraftItemStack.asNMSCopy(event.getItem()); ++ } else { ++ return itemstack; ++ } ++ } ++ ++ private static Inventory getInventory(final Container container) { ++ final Inventory sourceInventory; ++ if (container instanceof CompoundContainer compoundContainer) { ++ // Have to special-case large chests as they work oddly ++ sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer); ++ } else if (container instanceof BlockEntity blockEntity) { ++ sourceInventory = blockEntity.getOwner(false).getInventory(); ++ } else if (container.getOwner() != null) { ++ sourceInventory = container.getOwner().getInventory(); ++ } else { ++ sourceInventory = new CraftInventory(container); ++ } ++ return sourceInventory; ++ } ++ ++ private static void cooldownHopper(final Hopper hopper) { ++ if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) { ++ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer); ++ } ++ } ++ ++ private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { ++ if (iinventory instanceof WorldlyContainer) { ++ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { ++ if (!test.test(iinventory.getItem(i), i)) { ++ return false; ++ } ++ } ++ } else { ++ int size = iinventory.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (!test.test(iinventory.getItem(i), i)) { ++ return false; ++ } ++ } ++ } ++ return true; ++ } ++ ++ private static boolean anyMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { ++ if (iinventory instanceof WorldlyContainer) { ++ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { ++ if (test.test(iinventory.getItem(i), i)) { ++ return true; ++ } ++ } ++ } else { ++ int size = iinventory.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (test.test(iinventory.getItem(i), i)) { ++ return true; ++ } ++ } ++ } ++ return true; ++ } ++ private static final java.util.function.BiPredicate STACK_SIZE_TEST = (itemstack, i) -> itemstack.getCount() >= itemstack.getMaxStackSize(); ++ private static final java.util.function.BiPredicate IS_EMPTY_TEST = (itemstack, i) -> itemstack.isEmpty(); ++ // Paper end - Perf: Optimize Hoppers ++ + private static boolean ejectItems(Level world, BlockPos pos, HopperBlockEntity blockEntity) { + Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity); + +@@ -209,47 +443,50 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (HopperBlockEntity.isFullContainer(iinventory, enumdirection)) { + return false; + } else { +- for (int i = 0; i < blockEntity.getContainerSize(); ++i) { +- ItemStack itemstack = blockEntity.getItem(i); +- +- if (!itemstack.isEmpty()) { +- int j = itemstack.getCount(); +- // CraftBukkit start - Call event when pushing items into other inventories +- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot +- +- Inventory destinationInventory; +- // Have to special case large chests as they work oddly +- if (iinventory instanceof CompoundContainer) { +- destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); +- } else if (iinventory.getOwner() != null) { +- destinationInventory = iinventory.getOwner().getInventory(); +- } else { +- destinationInventory = new CraftInventory(iinventory); +- } +- +- InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true); +- world.getCraftServer().getPluginManager().callEvent(event); +- if (event.isCancelled()) { +- blockEntity.setItem(i, itemstack); +- blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot +- return false; +- } +- ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); +- // CraftBukkit end +- +- if (itemstack1.isEmpty()) { +- iinventory.setChanged(); +- return true; +- } +- +- itemstack.setCount(j); +- if (j == 1) { +- blockEntity.setItem(i, itemstack); +- } +- } +- } +- +- return false; ++ // Paper start - Perf: Optimize Hoppers ++ return hopperPush(world, iinventory, enumdirection, blockEntity); ++ //for (int i = 0; i < blockEntity.getContainerSize(); ++i) { ++ // ItemStack itemstack = blockEntity.getItem(i); ++ ++ // if (!itemstack.isEmpty()) { ++ // int j = itemstack.getCount(); ++ // // CraftBukkit start - Call event when pushing items into other inventories ++ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot ++ ++ // Inventory destinationInventory; ++ // // Have to special case large chests as they work oddly ++ // if (iinventory instanceof CompoundContainer) { ++ // destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); ++ // } else if (iinventory.getOwner() != null) { ++ // destinationInventory = iinventory.getOwner().getInventory(); ++ // } else { ++ // destinationInventory = new CraftInventory(iinventory); ++ // } ++ ++ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentityhopper.getOwner().getInventory(), oitemstack, destinationInventory, true); ++ // world.getCraftServer().getPluginManager().callEvent(event); ++ // if (event.isCancelled()) { ++ // blockEntity.setItem(i, itemstack); ++ // blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot ++ // return false; ++ // } ++ // ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); ++ // // CraftBukkit end ++ ++ // if (itemstack1.isEmpty()) { ++ // iinventory.setChanged(); ++ // return true; ++ // } ++ ++ // itemstack.setCount(j); ++ // if (j == 1) { ++ // blockEntity.setItem(i, itemstack); ++ // } ++ // } ++ //} ++ ++ // return false; ++ // Paper end - Perf: Optimize Hoppers + } + } + } +@@ -300,7 +537,6 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + return false; + } + } +- + return true; + } + +@@ -311,6 +547,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + if (iinventory != null) { + Direction enumdirection = Direction.DOWN; ++ skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers + int[] aint = HopperBlockEntity.getSlots(iinventory, enumdirection); + int i = aint.length; + +@@ -346,44 +583,47 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + ItemStack itemstack = iinventory.getItem(i); + + if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) { +- int j = itemstack.getCount(); +- // CraftBukkit start - Call event on collection of items from inventories into the hopper +- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot +- +- Inventory sourceInventory; +- // Have to special case large chests as they work oddly +- if (iinventory instanceof CompoundContainer) { +- sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); +- } else if (iinventory.getOwner() != null) { +- sourceInventory = iinventory.getOwner().getInventory(); +- } else { +- sourceInventory = new CraftInventory(iinventory); +- } +- +- InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); +- +- Bukkit.getServer().getPluginManager().callEvent(event); +- if (event.isCancelled()) { +- iinventory.setItem(i, itemstack); +- +- if (ihopper instanceof HopperBlockEntity) { +- ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot +- } +- +- return false; +- } +- ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); +- // CraftBukkit end +- +- if (itemstack1.isEmpty()) { +- iinventory.setChanged(); +- return true; +- } +- +- itemstack.setCount(j); +- if (j == 1) { +- iinventory.setItem(i, itemstack); +- } ++ // Paper start - Perf: Optimize Hoppers ++ return hopperPull(world, ihopper, iinventory, itemstack, i); ++ // int j = itemstack.getCount(); ++ // // CraftBukkit start - Call event on collection of items from inventories into the hopper ++ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot ++ ++ // Inventory sourceInventory; ++ // // Have to special case large chests as they work oddly ++ // if (iinventory instanceof CompoundContainer) { ++ // sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); ++ // } else if (iinventory.getOwner() != null) { ++ // sourceInventory = iinventory.getOwner().getInventory(); ++ // } else { ++ // sourceInventory = new CraftInventory(iinventory); ++ // } ++ ++ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); ++ ++ // Bukkit.getServer().getPluginManager().callEvent(event); ++ // if (event.isCancelled()) { ++ // iinventory.setItem(i, itemstack); ++ ++ // if (ihopper instanceof HopperBlockEntity) { ++ // ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot ++ // } ++ ++ // return false; ++ // } ++ // ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); ++ // // CraftBukkit end ++ ++ // if (itemstack1.isEmpty()) { ++ // iinventory.setChanged(); ++ // return true; ++ // } ++ ++ // itemstack.setCount(j); ++ // if (j == 1) { ++ // iinventory.setItem(i, itemstack); ++ // } ++ // Paper end - Perf: Optimize Hoppers + } + + return false; +@@ -392,12 +632,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + public static boolean addItem(Container inventory, ItemEntity itemEntity) { + boolean flag = false; + // CraftBukkit start +- InventoryPickupItemEvent event = new InventoryPickupItemEvent(inventory.getOwner().getInventory(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); ++ if (InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers ++ InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation + itemEntity.level().getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + // CraftBukkit end ++ } // Paper - Perf: Optimize Hoppers + ItemStack itemstack = itemEntity.getItem().copy(); + ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null); + +@@ -491,7 +733,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + stack = stack.split(to.getMaxStackSize()); + } + // Spigot end ++ ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers + to.setItem(slot, stack); ++ ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers + stack = leftover; // Paper - Make hoppers respect inventory max stack size + flag = true; + } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { +@@ -571,14 +815,20 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + public static Container getContainerAt(Level world, BlockPos pos) { +- return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D); ++ return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true); + } + + @Nullable + private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z) { ++ // Paper start - Perf: Optimize Hoppers ++ return HopperBlockEntity.getContainerAt(world, pos, state, x, y, z, false); ++ } ++ @Nullable ++ private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z, boolean optimizeEntities) { ++ // Paper end - Perf: Optimize Hoppers + Container iinventory = HopperBlockEntity.getBlockContainer(world, pos, state); + +- if (iinventory == null) { ++ if (iinventory == null && (!optimizeEntities || !world.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers + iinventory = HopperBlockEntity.getEntityContainer(world, x, y, z); + } + +@@ -613,13 +863,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + private static Container getEntityContainer(Level world, double x, double y, double z) { +- List list = world.getEntities((Entity) null, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); ++ List list = world.getEntitiesOfClass((Class) Container.class, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper - Perf: Optimize hoppers + + return !list.isEmpty() ? (Container) list.get(world.random.nextInt(list.size())) : null; + } + + private static boolean canMergeItems(ItemStack first, ItemStack second) { +- return first.getCount() <= first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); ++ return first.getCount() < first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?! + } + + @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 13c9a68b604d4c7c6e09e72b3cea7ab2214b06ab..e2752752417c50b06f7c15b7d00bda0eaad3b0ae 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 +@@ -53,7 +53,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc + + @Override + public ItemStack getItem(int slot) { +- this.unpackLootTable(null); ++ if (slot == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers + return super.getItem(slot); + } + diff --git a/patches/server/1010-Entity-load-save-limit-per-chunk.patch b/patches/server/1010-Entity-load-save-limit-per-chunk.patch new file mode 100644 index 0000000000..380beda077 --- /dev/null +++ b/patches/server/1010-Entity-load-save-limit-per-chunk.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Wed, 18 Nov 2020 20:52:25 -0800 +Subject: [PATCH] Entity load/save limit per chunk + +Adds a config option to limit the number of entities saved and loaded +to a chunk. The default values of -1 disable the limit. Although +defaults are only included for certain entites, this allows setting +limits for any entity type. + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +index 997b05167c19472acb98edac32d4548cc65efa8e..87d2b3ec165e2e9e4bdbedd7adddaa2130ed507b 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -100,7 +100,18 @@ public final class ChunkEntitySlices { + } + + final ListTag entitiesTag = new ListTag(); ++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + for (final Entity entity : entities) { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ break; ++ } ++ savedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + entitiesTag.add(compoundTag); +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 842b0cec0397d7ae5166617627340ffac0e35db1..cb61462d4691a055a4b25f7b953609d8a154fdfe 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -649,9 +649,20 @@ public class EntityType implements FeatureElement, EntityTypeT + final Spliterator spliterator = entityNbtList.spliterator(); + + return StreamSupport.stream(new Spliterator() { ++ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + public boolean tryAdvance(Consumer consumer) { + return spliterator.tryAdvance((nbtbase) -> { + EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, (entity) -> { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return null; ++ } ++ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + consumer.accept(entity); + return entity; + }); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 503ac0374e0c9f9993ad37bb8bd8cf1570d3615a..d4a505ef4af9ded02aeb1a817bcbe5b1a912a5b3 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -92,7 +92,18 @@ public class EntityStorage implements EntityPersistentStorage { + } + } else { + ListTag listTag = new ListTag(); ++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + dataList.getEntities().forEach(entity -> { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = this.level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return; ++ } ++ savedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + CompoundTag compoundTagx = new CompoundTag(); + if (entity.save(compoundTagx)) { + listTag.add(compoundTagx); diff --git a/patches/server/1010-Optimize-Voxel-Shape-Merging.patch b/patches/server/1010-Optimize-Voxel-Shape-Merging.patch deleted file mode 100644 index 4ec779560c..0000000000 --- a/patches/server/1010-Optimize-Voxel-Shape-Merging.patch +++ /dev/null @@ -1,123 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sun, 3 May 2020 22:35:09 -0400 -Subject: [PATCH] Optimize Voxel Shape Merging - -This method shows up as super hot in profiler, and also a high "self" time. - -Upon analyzing, it appears most usages of this method fall down to the final -else statement of the nasty ternary. - -Upon even further analyzation, it appears then the majority of those have a -consistent list 1.... One with Infinity head and Tails. - -First optimization is to detect these infinite states and immediately return that -VoxelShapeMergerList so we can avoid testing the rest for most cases. - -Break the method into 2 to help the JVM promote inlining of this fast path. - -Then it was also noticed that VoxelShapeMergerList constructor is also a hotspot -with a high self time... - -Well, knowing that in most cases our list 1 is actualy the same value, it allows -us to know that with an infinite list1, the result on the merger is essentially -list2 as the final values. - -This let us analyze the 2 potential states (Infinite with 2 sources or 4 sources) -and compute a deterministic result for the MergerList values. - -Additionally, this lets us avoid even allocating new objects for this too, further -reducing memory usage. - -diff --git a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -index e164c524aef4fa81fe96ac43454eecff1c38b9c1..9cfbbc61fcfc678f0988d6d45c7994d128051744 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -@@ -10,12 +10,33 @@ public class IndirectMerger implements IndexMerger { - private final int[] firstIndices; - private final int[] secondIndices; - private final int resultLength; -+ // Paper start -+ private static final int[] INFINITE_B_1 = new int[]{1, 1}; -+ private static final int[] INFINITE_B_0 = new int[]{0, 0}; -+ private static final int[] INFINITE_C = new int[]{0, 1}; -+ // Paper end - - public IndirectMerger(DoubleList first, DoubleList second, boolean includeFirstOnly, boolean includeSecondOnly) { - double d = Double.NaN; - int i = first.size(); - int j = second.size(); - int k = i + j; -+ // Paper start - optimize common path of infinity doublelist -+ int size = first.size(); -+ double tail = first.getDouble(size - 1); -+ double head = first.getDouble(0); -+ if (head == Double.NEGATIVE_INFINITY && tail == Double.POSITIVE_INFINITY && !includeFirstOnly && !includeSecondOnly && (size == 2 || size == 4)) { -+ this.result = second.toDoubleArray(); -+ this.resultLength = second.size(); -+ if (size == 2) { -+ this.firstIndices = INFINITE_B_0; -+ } else { -+ this.firstIndices = INFINITE_B_1; -+ } -+ this.secondIndices = INFINITE_C; -+ return; -+ } -+ // Paper end - this.result = new double[k]; - this.firstIndices = new int[k]; - this.secondIndices = new int[k]; -diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -index 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -@@ -286,9 +286,21 @@ public final class Shapes { - } - - @VisibleForTesting -- protected static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { -+ private static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { // Paper - private -+ // Paper start - fast track the most common scenario -+ // doublelist is usually a DoubleArrayList with Infinite head/tails that falls to the final else clause -+ // This is actually the most common path, so jump to it straight away -+ if (first.getDouble(0) == Double.NEGATIVE_INFINITY && first.getDouble(first.size() - 1) == Double.POSITIVE_INFINITY) { -+ return new IndirectMerger(first, second, includeFirst, includeSecond); -+ } -+ // Split out rest to hopefully inline the above -+ return lessCommonMerge(size, first, second, includeFirst, includeSecond); -+ } -+ -+ private static IndexMerger lessCommonMerge(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { - int i = first.size() - 1; - int j = second.size() - 1; -+ // Paper note - Rewrite below as optimized order if instead of nasty ternary - if (first instanceof CubePointRange && second instanceof CubePointRange) { - long l = lcm(i, j); - if ((long)size * l <= 256L) { -@@ -296,15 +308,22 @@ public final class Shapes { - } - } - -- if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { -+ // Paper start - Identical happens more often than Disjoint -+ if (i == j && Objects.equals(first, second)) { -+ if (first instanceof IdenticalMerger) { -+ return (IndexMerger) first; -+ } else if (second instanceof IdenticalMerger) { -+ return (IndexMerger) second; -+ } -+ return new IdenticalMerger(first); -+ } else if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { - return new NonOverlappingMerger(first, second, false); - } else if (second.getDouble(j) < first.getDouble(0) - 1.0E-7) { - return new NonOverlappingMerger(second, first, true); - } else { -- return (IndexMerger)(i == j && Objects.equals(first, second) -- ? new IdenticalMerger(first) -- : new IndirectMerger(first, second, includeFirst, includeSecond)); -+ return new IndirectMerger(first, second, includeFirst, includeSecond); - } -+ // Paper end - } - - public interface DoubleLineConsumer { diff --git a/patches/server/1011-Optimize-Bit-Operations-by-inlining.patch b/patches/server/1011-Optimize-Bit-Operations-by-inlining.patch deleted file mode 100644 index b7d93a56b9..0000000000 --- a/patches/server/1011-Optimize-Bit-Operations-by-inlining.patch +++ /dev/null @@ -1,213 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 4 Jun 2020 02:24:49 -0400 -Subject: [PATCH] Optimize Bit Operations by inlining - -Inline bit operations and reduce instruction count to make these hot -operations faster - -diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java -index 12ff8886bb53ca15db745989c25b9bd2f45335e4..2767d6f97e8b314d23a8e62f22dfd396f5660d31 100644 ---- a/src/main/java/net/minecraft/core/BlockPos.java -+++ b/src/main/java/net/minecraft/core/BlockPos.java -@@ -50,15 +50,16 @@ public class BlockPos extends Vec3i { - }; - private static final Logger LOGGER = LogUtils.getLogger(); - public static final BlockPos ZERO = new BlockPos(0, 0, 0); -- private static final int PACKED_X_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000)); -- private static final int PACKED_Z_LENGTH = PACKED_X_LENGTH; -- public static final int PACKED_Y_LENGTH = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; -- private static final long PACKED_X_MASK = (1L << PACKED_X_LENGTH) - 1L; -- private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L; -- private static final long PACKED_Z_MASK = (1L << PACKED_Z_LENGTH) - 1L; -- private static final int Y_OFFSET = 0; -- private static final int Z_OFFSET = PACKED_Y_LENGTH; -- private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_Z_LENGTH; -+ // Paper start - Optimize Bit Operations by inlining -+ private static final int PACKED_X_LENGTH = 26; -+ private static final int PACKED_Z_LENGTH = 26; -+ public static final int PACKED_Y_LENGTH = 12; -+ private static final long PACKED_X_MASK = 67108863; -+ private static final long PACKED_Y_MASK = 4095; -+ private static final long PACKED_Z_MASK = 67108863; -+ private static final int Z_OFFSET = 12; -+ private static final int X_OFFSET = 38; -+ // Paper end - Optimize Bit Operations by inlining - - public BlockPos(int x, int y, int z) { - super(x, y, z); -@@ -68,28 +69,29 @@ public class BlockPos extends Vec3i { - this(pos.getX(), pos.getY(), pos.getZ()); - } - -+ public static long getAdjacent(int baseX, int baseY, int baseZ, Direction enumdirection) { return asLong(baseX + enumdirection.getStepX(), baseY + enumdirection.getStepY(), baseZ + enumdirection.getStepZ()); } // Paper - public static long offset(long value, Direction direction) { - return offset(value, direction.getStepX(), direction.getStepY(), direction.getStepZ()); - } - - public static long offset(long value, int x, int y, int z) { -- return asLong(getX(value) + x, getY(value) + y, getZ(value) + z); -+ return asLong((int) (value >> 38) + x, (int) ((value << 52) >> 52) + y, (int) ((value << 26) >> 38) + z); // Paper - simplify/inline - } - - public static int getX(long packedPos) { -- return (int)(packedPos << 64 - X_OFFSET - PACKED_X_LENGTH >> 64 - PACKED_X_LENGTH); -+ return (int) (packedPos >> 38); // Paper - simplify/inline - } - - public static int getY(long packedPos) { -- return (int)(packedPos << 64 - PACKED_Y_LENGTH >> 64 - PACKED_Y_LENGTH); -+ return (int) ((packedPos << 52) >> 52); // Paper - simplify/inline - } - - public static int getZ(long packedPos) { -- return (int)(packedPos << 64 - Z_OFFSET - PACKED_Z_LENGTH >> 64 - PACKED_Z_LENGTH); -+ return (int) ((packedPos << 26) >> 38); // Paper - simplify/inline - } - - public static BlockPos of(long packedPos) { -- return new BlockPos(getX(packedPos), getY(packedPos), getZ(packedPos)); -+ return new BlockPos((int) (packedPos >> 38), (int) ((packedPos << 52) >> 52), (int) ((packedPos << 26) >> 38)); // Paper - simplify/inline - } - - public static BlockPos containing(double x, double y, double z) { -@@ -113,10 +115,7 @@ public class BlockPos extends Vec3i { - } - - public static long asLong(int x, int y, int z) { -- long l = 0L; -- l |= ((long)x & PACKED_X_MASK) << X_OFFSET; -- l |= ((long)y & PACKED_Y_MASK) << 0; -- return l | ((long)z & PACKED_Z_MASK) << Z_OFFSET; -+ return (((long) x & (long) 67108863) << 38) | (((long) y & (long) 4095)) | (((long) z & (long) 67108863) << 12); // Paper - inline constants and simplify - } - - public static long getFlatIndex(long y) { -diff --git a/src/main/java/net/minecraft/core/SectionPos.java b/src/main/java/net/minecraft/core/SectionPos.java -index 27e0d53d5893a13a340deddc93a1128968db7e5b..fe3577e533fb829c85fd4881b1bcca3b70aaf1a5 100644 ---- a/src/main/java/net/minecraft/core/SectionPos.java -+++ b/src/main/java/net/minecraft/core/SectionPos.java -@@ -38,7 +38,7 @@ public class SectionPos extends Vec3i { - } - - public static SectionPos of(BlockPos pos) { -- return new SectionPos(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); -+ return new SectionPos(pos.getX() >> 4, pos.getY() >> 4, pos.getZ() >> 4); // Paper - } - - public static SectionPos of(ChunkPos chunkPos, int y) { -@@ -54,7 +54,7 @@ public class SectionPos extends Vec3i { - } - - public static SectionPos of(long packed) { -- return new SectionPos(x(packed), y(packed), z(packed)); -+ return new SectionPos((int) (packed >> 42), (int) (packed << 44 >> 44), (int) (packed << 22 >> 42)); // Paper - } - - public static SectionPos bottomOf(ChunkAccess chunk) { -@@ -65,8 +65,16 @@ public class SectionPos extends Vec3i { - return offset(packed, direction.getStepX(), direction.getStepY(), direction.getStepZ()); - } - -+ // Paper start -+ public static long getAdjacentFromBlockPos(int x, int y, int z, Direction enumdirection) { -+ return (((long) ((x >> 4) + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y >> 4) + enumdirection.getStepY()) & 1048575L)) | (((long) ((z >> 4) + enumdirection.getStepZ()) & 4194303L) << 20); -+ } -+ public static long getAdjacentFromSectionPos(int x, int y, int z, Direction enumdirection) { -+ return (((long) (x + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y) + enumdirection.getStepY()) & 1048575L)) | (((long) (z + enumdirection.getStepZ()) & 4194303L) << 20); -+ } -+ // Paper end - public static long offset(long packed, int x, int y, int z) { -- return asLong(x(packed) + x, y(packed) + y, z(packed) + z); -+ return (((long) ((int) (packed >> 42) + x) & 4194303L) << 42) | (((long) ((int) (packed << 44 >> 44) + y) & 1048575L)) | (((long) ((int) (packed << 22 >> 42) + z) & 4194303L) << 20); // Simplify to reduce instruction count - } - - public static int posToSectionCoord(double coord) { -@@ -86,10 +94,7 @@ public class SectionPos extends Vec3i { - } - - public static short sectionRelativePos(BlockPos pos) { -- int i = sectionRelative(pos.getX()); -- int j = sectionRelative(pos.getY()); -- int k = sectionRelative(pos.getZ()); -- return (short)(i << 8 | k << 4 | j << 0); -+ return (short) ((pos.getX() & 15) << 8 | (pos.getZ() & 15) << 4 | pos.getY() & 15); // Paper - simplify/inline - } - - public static int sectionRelativeX(short packedLocalPos) { -@@ -152,16 +157,16 @@ public class SectionPos extends Vec3i { - return this.getZ(); - } - -- public int minBlockX() { -- return sectionToBlockCoord(this.x()); -+ public final int minBlockX() { // Paper - make final -+ return this.getX() << 4; // Paper - inline - } - -- public int minBlockY() { -- return sectionToBlockCoord(this.y()); -+ public final int minBlockY() { // Paper - make final -+ return this.getY() << 4; // Paper - inline - } - -- public int minBlockZ() { -- return sectionToBlockCoord(this.z()); -+ public int minBlockZ() { // Paper - make final -+ return this.getZ() << 4; // Paper - inline - } - - public int maxBlockX() { -@@ -177,7 +182,8 @@ public class SectionPos extends Vec3i { - } - - public static long blockToSection(long blockPos) { -- return asLong(blockToSectionCoord(BlockPos.getX(blockPos)), blockToSectionCoord(BlockPos.getY(blockPos)), blockToSectionCoord(BlockPos.getZ(blockPos))); -+ // b(a(BlockPosition.b(i)), a(BlockPosition.c(i)), a(BlockPosition.d(i))); -+ return (((long) (int) (blockPos >> 42) & 4194303L) << 42) | (((long) (int) ((blockPos << 52) >> 56) & 1048575L)) | (((long) (int) ((blockPos << 26) >> 42) & 4194303L) << 20); // Simplify to reduce instruction count - } - - public static long getZeroNode(int x, int z) { -@@ -205,15 +211,18 @@ public class SectionPos extends Vec3i { - return asLong(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); - } - -+ // Paper start -+ public static long blockPosAsSectionLong(int i, int j, int k) { -+ return (((long) (i >> 4) & 4194303L) << 42) | (((long) (j >> 4) & 1048575L)) | (((long) (k >> 4) & 4194303L) << 20); -+ } -+ // Paper end -+ - public static long asLong(int x, int y, int z) { -- long l = 0L; -- l |= ((long)x & 4194303L) << 42; -- l |= ((long)y & 1048575L) << 0; -- return l | ((long)z & 4194303L) << 20; -+ return (((long) x & 4194303L) << 42) | (((long) y & 1048575L)) | (((long) z & 4194303L) << 20); // Paper - Simplify to reduce instruction count - } - - public long asLong() { -- return asLong(this.x(), this.y(), this.z()); -+ return (((long) getX() & 4194303L) << 42) | (((long) getY() & 1048575L)) | (((long) getZ() & 4194303L) << 20); // Paper - Simplify to reduce instruction count - } - - @Override -@@ -226,16 +235,11 @@ public class SectionPos extends Vec3i { - } - - public static Stream cube(SectionPos center, int radius) { -- int i = center.x(); -- int j = center.y(); -- int k = center.z(); -- return betweenClosedStream(i - radius, j - radius, k - radius, i + radius, j + radius, k + radius); -+ return betweenClosedStream(center.getX() - radius, center.getY() - radius, center.getZ() - radius, center.getX() + radius, center.getY() + radius, center.getZ() + radius); // Paper - simplify/inline - } - - public static Stream aroundChunk(ChunkPos center, int radius, int minY, int maxY) { -- int i = center.x; -- int j = center.z; -- return betweenClosedStream(i - radius, minY, j - radius, i + radius, maxY - 1, j + radius); -+ return betweenClosedStream(center.x - radius, 0, center.z - radius, center.x + radius, 15, center.z + radius); // Paper - simplify/inline - } - - public static Stream betweenClosedStream(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { diff --git a/patches/server/1011-Optimize-Voxel-Shape-Merging.patch b/patches/server/1011-Optimize-Voxel-Shape-Merging.patch new file mode 100644 index 0000000000..4ec779560c --- /dev/null +++ b/patches/server/1011-Optimize-Voxel-Shape-Merging.patch @@ -0,0 +1,123 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sun, 3 May 2020 22:35:09 -0400 +Subject: [PATCH] Optimize Voxel Shape Merging + +This method shows up as super hot in profiler, and also a high "self" time. + +Upon analyzing, it appears most usages of this method fall down to the final +else statement of the nasty ternary. + +Upon even further analyzation, it appears then the majority of those have a +consistent list 1.... One with Infinity head and Tails. + +First optimization is to detect these infinite states and immediately return that +VoxelShapeMergerList so we can avoid testing the rest for most cases. + +Break the method into 2 to help the JVM promote inlining of this fast path. + +Then it was also noticed that VoxelShapeMergerList constructor is also a hotspot +with a high self time... + +Well, knowing that in most cases our list 1 is actualy the same value, it allows +us to know that with an infinite list1, the result on the merger is essentially +list2 as the final values. + +This let us analyze the 2 potential states (Infinite with 2 sources or 4 sources) +and compute a deterministic result for the MergerList values. + +Additionally, this lets us avoid even allocating new objects for this too, further +reducing memory usage. + +diff --git a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java +index e164c524aef4fa81fe96ac43454eecff1c38b9c1..9cfbbc61fcfc678f0988d6d45c7994d128051744 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java +@@ -10,12 +10,33 @@ public class IndirectMerger implements IndexMerger { + private final int[] firstIndices; + private final int[] secondIndices; + private final int resultLength; ++ // Paper start ++ private static final int[] INFINITE_B_1 = new int[]{1, 1}; ++ private static final int[] INFINITE_B_0 = new int[]{0, 0}; ++ private static final int[] INFINITE_C = new int[]{0, 1}; ++ // Paper end + + public IndirectMerger(DoubleList first, DoubleList second, boolean includeFirstOnly, boolean includeSecondOnly) { + double d = Double.NaN; + int i = first.size(); + int j = second.size(); + int k = i + j; ++ // Paper start - optimize common path of infinity doublelist ++ int size = first.size(); ++ double tail = first.getDouble(size - 1); ++ double head = first.getDouble(0); ++ if (head == Double.NEGATIVE_INFINITY && tail == Double.POSITIVE_INFINITY && !includeFirstOnly && !includeSecondOnly && (size == 2 || size == 4)) { ++ this.result = second.toDoubleArray(); ++ this.resultLength = second.size(); ++ if (size == 2) { ++ this.firstIndices = INFINITE_B_0; ++ } else { ++ this.firstIndices = INFINITE_B_1; ++ } ++ this.secondIndices = INFINITE_C; ++ return; ++ } ++ // Paper end + this.result = new double[k]; + this.firstIndices = new int[k]; + this.secondIndices = new int[k]; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -286,9 +286,21 @@ public final class Shapes { + } + + @VisibleForTesting +- protected static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { ++ private static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { // Paper - private ++ // Paper start - fast track the most common scenario ++ // doublelist is usually a DoubleArrayList with Infinite head/tails that falls to the final else clause ++ // This is actually the most common path, so jump to it straight away ++ if (first.getDouble(0) == Double.NEGATIVE_INFINITY && first.getDouble(first.size() - 1) == Double.POSITIVE_INFINITY) { ++ return new IndirectMerger(first, second, includeFirst, includeSecond); ++ } ++ // Split out rest to hopefully inline the above ++ return lessCommonMerge(size, first, second, includeFirst, includeSecond); ++ } ++ ++ private static IndexMerger lessCommonMerge(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { + int i = first.size() - 1; + int j = second.size() - 1; ++ // Paper note - Rewrite below as optimized order if instead of nasty ternary + if (first instanceof CubePointRange && second instanceof CubePointRange) { + long l = lcm(i, j); + if ((long)size * l <= 256L) { +@@ -296,15 +308,22 @@ public final class Shapes { + } + } + +- if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { ++ // Paper start - Identical happens more often than Disjoint ++ if (i == j && Objects.equals(first, second)) { ++ if (first instanceof IdenticalMerger) { ++ return (IndexMerger) first; ++ } else if (second instanceof IdenticalMerger) { ++ return (IndexMerger) second; ++ } ++ return new IdenticalMerger(first); ++ } else if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { + return new NonOverlappingMerger(first, second, false); + } else if (second.getDouble(j) < first.getDouble(0) - 1.0E-7) { + return new NonOverlappingMerger(second, first, true); + } else { +- return (IndexMerger)(i == j && Objects.equals(first, second) +- ? new IdenticalMerger(first) +- : new IndirectMerger(first, second, includeFirst, includeSecond)); ++ return new IndirectMerger(first, second, includeFirst, includeSecond); + } ++ // Paper end + } + + public interface DoubleLineConsumer { diff --git a/patches/server/1012-Optimize-Bit-Operations-by-inlining.patch b/patches/server/1012-Optimize-Bit-Operations-by-inlining.patch new file mode 100644 index 0000000000..b7d93a56b9 --- /dev/null +++ b/patches/server/1012-Optimize-Bit-Operations-by-inlining.patch @@ -0,0 +1,213 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Thu, 4 Jun 2020 02:24:49 -0400 +Subject: [PATCH] Optimize Bit Operations by inlining + +Inline bit operations and reduce instruction count to make these hot +operations faster + +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 12ff8886bb53ca15db745989c25b9bd2f45335e4..2767d6f97e8b314d23a8e62f22dfd396f5660d31 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -50,15 +50,16 @@ public class BlockPos extends Vec3i { + }; + private static final Logger LOGGER = LogUtils.getLogger(); + public static final BlockPos ZERO = new BlockPos(0, 0, 0); +- private static final int PACKED_X_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000)); +- private static final int PACKED_Z_LENGTH = PACKED_X_LENGTH; +- public static final int PACKED_Y_LENGTH = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; +- private static final long PACKED_X_MASK = (1L << PACKED_X_LENGTH) - 1L; +- private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L; +- private static final long PACKED_Z_MASK = (1L << PACKED_Z_LENGTH) - 1L; +- private static final int Y_OFFSET = 0; +- private static final int Z_OFFSET = PACKED_Y_LENGTH; +- private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_Z_LENGTH; ++ // Paper start - Optimize Bit Operations by inlining ++ private static final int PACKED_X_LENGTH = 26; ++ private static final int PACKED_Z_LENGTH = 26; ++ public static final int PACKED_Y_LENGTH = 12; ++ private static final long PACKED_X_MASK = 67108863; ++ private static final long PACKED_Y_MASK = 4095; ++ private static final long PACKED_Z_MASK = 67108863; ++ private static final int Z_OFFSET = 12; ++ private static final int X_OFFSET = 38; ++ // Paper end - Optimize Bit Operations by inlining + + public BlockPos(int x, int y, int z) { + super(x, y, z); +@@ -68,28 +69,29 @@ public class BlockPos extends Vec3i { + this(pos.getX(), pos.getY(), pos.getZ()); + } + ++ public static long getAdjacent(int baseX, int baseY, int baseZ, Direction enumdirection) { return asLong(baseX + enumdirection.getStepX(), baseY + enumdirection.getStepY(), baseZ + enumdirection.getStepZ()); } // Paper + public static long offset(long value, Direction direction) { + return offset(value, direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + + public static long offset(long value, int x, int y, int z) { +- return asLong(getX(value) + x, getY(value) + y, getZ(value) + z); ++ return asLong((int) (value >> 38) + x, (int) ((value << 52) >> 52) + y, (int) ((value << 26) >> 38) + z); // Paper - simplify/inline + } + + public static int getX(long packedPos) { +- return (int)(packedPos << 64 - X_OFFSET - PACKED_X_LENGTH >> 64 - PACKED_X_LENGTH); ++ return (int) (packedPos >> 38); // Paper - simplify/inline + } + + public static int getY(long packedPos) { +- return (int)(packedPos << 64 - PACKED_Y_LENGTH >> 64 - PACKED_Y_LENGTH); ++ return (int) ((packedPos << 52) >> 52); // Paper - simplify/inline + } + + public static int getZ(long packedPos) { +- return (int)(packedPos << 64 - Z_OFFSET - PACKED_Z_LENGTH >> 64 - PACKED_Z_LENGTH); ++ return (int) ((packedPos << 26) >> 38); // Paper - simplify/inline + } + + public static BlockPos of(long packedPos) { +- return new BlockPos(getX(packedPos), getY(packedPos), getZ(packedPos)); ++ return new BlockPos((int) (packedPos >> 38), (int) ((packedPos << 52) >> 52), (int) ((packedPos << 26) >> 38)); // Paper - simplify/inline + } + + public static BlockPos containing(double x, double y, double z) { +@@ -113,10 +115,7 @@ public class BlockPos extends Vec3i { + } + + public static long asLong(int x, int y, int z) { +- long l = 0L; +- l |= ((long)x & PACKED_X_MASK) << X_OFFSET; +- l |= ((long)y & PACKED_Y_MASK) << 0; +- return l | ((long)z & PACKED_Z_MASK) << Z_OFFSET; ++ return (((long) x & (long) 67108863) << 38) | (((long) y & (long) 4095)) | (((long) z & (long) 67108863) << 12); // Paper - inline constants and simplify + } + + public static long getFlatIndex(long y) { +diff --git a/src/main/java/net/minecraft/core/SectionPos.java b/src/main/java/net/minecraft/core/SectionPos.java +index 27e0d53d5893a13a340deddc93a1128968db7e5b..fe3577e533fb829c85fd4881b1bcca3b70aaf1a5 100644 +--- a/src/main/java/net/minecraft/core/SectionPos.java ++++ b/src/main/java/net/minecraft/core/SectionPos.java +@@ -38,7 +38,7 @@ public class SectionPos extends Vec3i { + } + + public static SectionPos of(BlockPos pos) { +- return new SectionPos(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); ++ return new SectionPos(pos.getX() >> 4, pos.getY() >> 4, pos.getZ() >> 4); // Paper + } + + public static SectionPos of(ChunkPos chunkPos, int y) { +@@ -54,7 +54,7 @@ public class SectionPos extends Vec3i { + } + + public static SectionPos of(long packed) { +- return new SectionPos(x(packed), y(packed), z(packed)); ++ return new SectionPos((int) (packed >> 42), (int) (packed << 44 >> 44), (int) (packed << 22 >> 42)); // Paper + } + + public static SectionPos bottomOf(ChunkAccess chunk) { +@@ -65,8 +65,16 @@ public class SectionPos extends Vec3i { + return offset(packed, direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + ++ // Paper start ++ public static long getAdjacentFromBlockPos(int x, int y, int z, Direction enumdirection) { ++ return (((long) ((x >> 4) + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y >> 4) + enumdirection.getStepY()) & 1048575L)) | (((long) ((z >> 4) + enumdirection.getStepZ()) & 4194303L) << 20); ++ } ++ public static long getAdjacentFromSectionPos(int x, int y, int z, Direction enumdirection) { ++ return (((long) (x + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y) + enumdirection.getStepY()) & 1048575L)) | (((long) (z + enumdirection.getStepZ()) & 4194303L) << 20); ++ } ++ // Paper end + public static long offset(long packed, int x, int y, int z) { +- return asLong(x(packed) + x, y(packed) + y, z(packed) + z); ++ return (((long) ((int) (packed >> 42) + x) & 4194303L) << 42) | (((long) ((int) (packed << 44 >> 44) + y) & 1048575L)) | (((long) ((int) (packed << 22 >> 42) + z) & 4194303L) << 20); // Simplify to reduce instruction count + } + + public static int posToSectionCoord(double coord) { +@@ -86,10 +94,7 @@ public class SectionPos extends Vec3i { + } + + public static short sectionRelativePos(BlockPos pos) { +- int i = sectionRelative(pos.getX()); +- int j = sectionRelative(pos.getY()); +- int k = sectionRelative(pos.getZ()); +- return (short)(i << 8 | k << 4 | j << 0); ++ return (short) ((pos.getX() & 15) << 8 | (pos.getZ() & 15) << 4 | pos.getY() & 15); // Paper - simplify/inline + } + + public static int sectionRelativeX(short packedLocalPos) { +@@ -152,16 +157,16 @@ public class SectionPos extends Vec3i { + return this.getZ(); + } + +- public int minBlockX() { +- return sectionToBlockCoord(this.x()); ++ public final int minBlockX() { // Paper - make final ++ return this.getX() << 4; // Paper - inline + } + +- public int minBlockY() { +- return sectionToBlockCoord(this.y()); ++ public final int minBlockY() { // Paper - make final ++ return this.getY() << 4; // Paper - inline + } + +- public int minBlockZ() { +- return sectionToBlockCoord(this.z()); ++ public int minBlockZ() { // Paper - make final ++ return this.getZ() << 4; // Paper - inline + } + + public int maxBlockX() { +@@ -177,7 +182,8 @@ public class SectionPos extends Vec3i { + } + + public static long blockToSection(long blockPos) { +- return asLong(blockToSectionCoord(BlockPos.getX(blockPos)), blockToSectionCoord(BlockPos.getY(blockPos)), blockToSectionCoord(BlockPos.getZ(blockPos))); ++ // b(a(BlockPosition.b(i)), a(BlockPosition.c(i)), a(BlockPosition.d(i))); ++ return (((long) (int) (blockPos >> 42) & 4194303L) << 42) | (((long) (int) ((blockPos << 52) >> 56) & 1048575L)) | (((long) (int) ((blockPos << 26) >> 42) & 4194303L) << 20); // Simplify to reduce instruction count + } + + public static long getZeroNode(int x, int z) { +@@ -205,15 +211,18 @@ public class SectionPos extends Vec3i { + return asLong(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); + } + ++ // Paper start ++ public static long blockPosAsSectionLong(int i, int j, int k) { ++ return (((long) (i >> 4) & 4194303L) << 42) | (((long) (j >> 4) & 1048575L)) | (((long) (k >> 4) & 4194303L) << 20); ++ } ++ // Paper end ++ + public static long asLong(int x, int y, int z) { +- long l = 0L; +- l |= ((long)x & 4194303L) << 42; +- l |= ((long)y & 1048575L) << 0; +- return l | ((long)z & 4194303L) << 20; ++ return (((long) x & 4194303L) << 42) | (((long) y & 1048575L)) | (((long) z & 4194303L) << 20); // Paper - Simplify to reduce instruction count + } + + public long asLong() { +- return asLong(this.x(), this.y(), this.z()); ++ return (((long) getX() & 4194303L) << 42) | (((long) getY() & 1048575L)) | (((long) getZ() & 4194303L) << 20); // Paper - Simplify to reduce instruction count + } + + @Override +@@ -226,16 +235,11 @@ public class SectionPos extends Vec3i { + } + + public static Stream cube(SectionPos center, int radius) { +- int i = center.x(); +- int j = center.y(); +- int k = center.z(); +- return betweenClosedStream(i - radius, j - radius, k - radius, i + radius, j + radius, k + radius); ++ return betweenClosedStream(center.getX() - radius, center.getY() - radius, center.getZ() - radius, center.getX() + radius, center.getY() + radius, center.getZ() + radius); // Paper - simplify/inline + } + + public static Stream aroundChunk(ChunkPos center, int radius, int minY, int maxY) { +- int i = center.x; +- int j = center.z; +- return betweenClosedStream(i - radius, minY, j - radius, i + radius, maxY - 1, j + radius); ++ return betweenClosedStream(center.x - radius, 0, center.z - radius, center.x + radius, 15, center.z + radius); // Paper - simplify/inline + } + + public static Stream betweenClosedStream(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { diff --git a/patches/server/1012-Remove-streams-from-hot-code.patch b/patches/server/1012-Remove-streams-from-hot-code.patch deleted file mode 100644 index 4c6d94f7bf..0000000000 --- a/patches/server/1012-Remove-streams-from-hot-code.patch +++ /dev/null @@ -1,215 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: JRoy -Date: Wed, 1 Jul 2020 18:01:49 -0400 -Subject: [PATCH] Remove streams from hot code - -Co-authored-by: Bjarne Koll -Co-authored-by: Spottedleaf - -diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -index 6a270c9adb044a6e0b1c8e09b9106d51989fd761..d4581127366736c54f74e4ef7479236b18fb487d 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -@@ -57,7 +57,7 @@ public class GateBehavior implements BehaviorControl - if (this.hasRequiredMemories(entity)) { - this.status = Behavior.Status.RUNNING; - this.orderPolicy.apply(this.behaviors); -- this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); -+ this.runningPolicy.apply(this.behaviors, world, entity, time); // Paper - Perf: Remove streams from hot code - return true; - } else { - return false; -@@ -66,7 +66,13 @@ public class GateBehavior implements BehaviorControl - - @Override - public final void tickOrStop(ServerLevel world, E entity, long time) { -- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.tickOrStop(world, entity, time)); -+ // Paper start - Perf: Remove streams from hot code -+ for (final BehaviorControl task : this.behaviors) { -+ if (task.getStatus() == Behavior.Status.RUNNING) { -+ task.tickOrStop(world, entity, time); -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - if (this.behaviors.stream().noneMatch(task -> task.getStatus() == Behavior.Status.RUNNING)) { - this.doStop(world, entity, time); - } -@@ -75,8 +81,16 @@ public class GateBehavior implements BehaviorControl - @Override - public final void doStop(ServerLevel world, E entity, long time) { - this.status = Behavior.Status.STOPPED; -- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.doStop(world, entity, time)); -- this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); -+ // Paper start - Perf: Remove streams from hot code -+ for (final BehaviorControl task : this.behaviors) { -+ if (task.getStatus() == Behavior.Status.RUNNING) { -+ task.doStop(world, entity, time); -+ } -+ } -+ for (final MemoryModuleType exitErasedMemory : this.exitErasedMemories) { -+ entity.getBrain().eraseMemory(exitErasedMemory); -+ } -+ // Paper end - Perf: Remove streams from hot code - } - - @Override -@@ -111,18 +125,30 @@ public class GateBehavior implements BehaviorControl - - public static enum RunningPolicy { - RUN_ONE { -+ // Paper start - Perf: Remove streams from hot code - @Override -- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { -- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).filter(task -> task.tryStart(world, entity, time)).findFirst(); -+ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { -+ for (final BehaviorControl task : tasks) { -+ if (task.getStatus() == Behavior.Status.STOPPED && task.tryStart(world, entity, time)) { -+ break; -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - } - }, - TRY_ALL { -+ // Paper start - Perf: Remove streams from hot code - @Override -- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { -- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).forEach(task -> task.tryStart(world, entity, time)); -+ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { -+ for (final BehaviorControl task : tasks) { -+ if (task.getStatus() == Behavior.Status.STOPPED) { -+ task.tryStart(world, entity, time); -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - } - }; - -- public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); -+ public abstract void apply(ShufflingList> tasks, ServerLevel world, E entity, long time); // Paper - Perf: Remove streams from hot code - } - } -diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -index aa32804bc9affe9a615d3ffaa513f6f09aab3f32..c7f012674361a323c1efeca4660cd3f46d308ee1 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -+++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -@@ -59,8 +59,22 @@ public class GossipContainer { - return this.gossips.entrySet().stream().flatMap(entry -> entry.getValue().unpack(entry.getKey())); - } - -+ // Paper start - Perf: Remove streams from hot code -+ private List decompress() { -+ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); -+ for (Map.Entry entry : this.gossips.entrySet()) { -+ for (GossipContainer.GossipEntry cur : entry.getValue().decompress(entry.getKey())) { -+ if (cur.weightedValue() != 0) { -+ list.add(cur); -+ } -+ } -+ } -+ return list; -+ } -+ // Paper end - Perf: Remove streams from hot code -+ - private Collection selectGossipsForTransfer(RandomSource random, int count) { -- List list = this.unpack().toList(); -+ List list = this.decompress(); // Paper - Perf: Remove streams from hot code - if (list.isEmpty()) { - return Collections.emptyList(); - } else { -@@ -145,7 +159,7 @@ public class GossipContainer { - - public T store(DynamicOps ops) { - return GossipContainer.GossipEntry.LIST_CODEC -- .encodeStart(ops, this.unpack().toList()) -+ .encodeStart(ops, this.decompress()) // Paper - Perf: Remove streams from hot code - .resultOrPartial(error -> LOGGER.warn("Failed to serialize gossips: {}", error)) - .orElseGet(ops::emptyList); - } -@@ -172,12 +186,23 @@ public class GossipContainer { - final Object2IntMap entries = new Object2IntOpenHashMap<>(); - - public int weightedValue(Predicate gossipTypeFilter) { -- return this.entries -- .object2IntEntrySet() -- .stream() -- .filter(entry -> gossipTypeFilter.test(entry.getKey())) -- .mapToInt(entry -> entry.getIntValue() * entry.getKey().weight) -- .sum(); -+ // Paper start - Perf: Remove streams from hot code -+ int weight = 0; -+ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { -+ if (gossipTypeFilter.test(entry.getKey())) { -+ weight += entry.getIntValue() * entry.getKey().weight; -+ } -+ } -+ return weight; -+ } -+ -+ public List decompress(UUID uuid) { -+ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); -+ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { -+ list.add(new GossipContainer.GossipEntry(uuid, entry.getKey(), entry.getIntValue())); -+ } -+ return list; -+ // Paper end - Perf: Remove streams from hot code - } - - public Stream unpack(UUID target) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -index 28fd073ddad358e087e8c78985a97cad21be81b7..a5bd308d1b3ea5db185c06a287167d1d8894a987 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -@@ -24,13 +24,17 @@ public class NearestItemSensor extends Sensor { - @Override - protected void doTick(ServerLevel world, Mob entity) { - Brain brain = entity.getBrain(); -- List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true); -+ List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> itemEntity.closerThan(entity, MAX_DISTANCE_TO_WANTED_ITEM) && entity.wantsToPickUp(itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities - list.sort(Comparator.comparingDouble(entity::distanceToSqr)); -- Optional optional = list.stream() -- .filter(itemEntity -> entity.wantsToPickUp(itemEntity.getItem())) -- .filter(itemEntity -> itemEntity.closerThan(entity, 32.0)) -- .filter(entity::hasLineOfSight) -- .findFirst(); -- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional); -+ // Paper start - Perf: remove streams from hot code -+ ItemEntity nearest = null; -+ for (ItemEntity entityItem : list) { -+ if (entity.hasLineOfSight(entityItem)) { // Paper - Perf: Move predicate into getEntities -+ nearest = entityItem; -+ break; -+ } -+ } -+ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); -+ // Paper end - Perf: remove streams from hot code - } - } -diff --git a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -index 04552e2cf3de2dc41dfe7e7a2e88b200eaaf280b..ca93a97256350789ca56f910862c9d717ca7670b 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -@@ -35,9 +35,10 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { - int j = pos.getMinBlockZ(); - ObjectList objectList = new ObjectArrayList<>(10); - ObjectList objectList2 = new ObjectArrayList<>(32); -- world.startsForStructure(pos, structure -> structure.terrainAdaptation() != TerrainAdjustment.NONE) -- .forEach( -- start -> { -+ // Paper start - Perf: Remove streams from hot code -+ for (net.minecraft.world.level.levelgen.structure.StructureStart start : world.startsForStructure(pos, (structure) -> { -+ return structure.terrainAdaptation() != TerrainAdjustment.NONE; -+ })) { // Paper end - Perf: Remove streams from hot code - TerrainAdjustment terrainAdjustment = start.getStructure().terrainAdaptation(); - - for (StructurePiece structurePiece : start.getPieces()) { -@@ -65,8 +66,7 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { - } - } - } -- } -- ); -+ } // Paper - Perf: Remove streams from hot code - return new Beardifier(objectList.iterator(), objectList2.iterator()); - } - diff --git a/patches/server/1013-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch b/patches/server/1013-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch deleted file mode 100644 index 6fc4445bbf..0000000000 --- a/patches/server/1013-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch +++ /dev/null @@ -1,133 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Tue, 4 Aug 2020 22:24:15 +0200 -Subject: [PATCH] Optimize Pathfinder - Remove Streams / Optimized collections - -I utilized the IDE to convert streams to non streams code, so shouldn't -be any risk of behavior change. Only did minor optimization of the -generated code set to remove unnecessary things. - -I expect us to just drop this patch on next major update and re-apply -it with the IDE again and re-apply the collections optimization. - -Optimize collection by creating a list instead of a set of the key and value. - -This lets us get faster foreach iteration, as well as avoids map lookups on -the values when needed. - -diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -index 8a483c8d2a54617d78af19de32fa0ded71b3223a..18bbb3f8f99849333ff4bc020c8ce758a69312a5 100644 ---- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -+++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -@@ -38,8 +38,12 @@ public class PathFinder { - if (node == null) { - return null; - } else { -- Map map = positions.stream() -- .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget((double)pos.getX(), (double)pos.getY(), (double)pos.getZ()), Function.identity())); -+ // Paper start - Perf: remove streams and optimize collection -+ List> map = Lists.newArrayList(); -+ for (final BlockPos pos : positions) { -+ map.add(new java.util.AbstractMap.SimpleEntry<>(this.nodeEvaluator.getTarget(pos.getX(), pos.getY(), pos.getZ()), pos)); -+ } -+ // Paper end - Perf: remove streams and optimize collection - Path path = this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); - this.nodeEvaluator.done(); - return path; -@@ -47,18 +51,19 @@ public class PathFinder { - } - - @Nullable -- private Path findPath(ProfilerFiller profiler, Node startNode, Map positions, float followRange, int distance, float rangeMultiplier) { -+ // Paper start - Perf: remove streams and optimize collection -+ private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { - profiler.push("find_path"); - profiler.markForCharting(MetricCategory.PATH_FINDING); -- Set set = positions.keySet(); -+ // Set set = positions.keySet(); - startNode.g = 0.0F; -- startNode.h = this.getBestH(startNode, set); -+ startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection - startNode.f = startNode.h; - this.openSet.clear(); - this.openSet.insert(startNode); -- Set set2 = ImmutableSet.of(); -+ // Set set2 = ImmutableSet.of(); // Paper - unused - diff on change - int i = 0; -- Set set3 = Sets.newHashSetWithExpectedSize(set.size()); -+ List> entryList = Lists.newArrayListWithExpectedSize(positions.size()); // Paper - optimize collection - int j = (int)((float)this.maxVisitedNodes * rangeMultiplier); - - while (!this.openSet.isEmpty()) { -@@ -69,14 +74,18 @@ public class PathFinder { - Node node = this.openSet.pop(); - node.closed = true; - -- for (Target target : set) { -+ // Paper start - optimize collection -+ for (int i1 = 0; i1 < positions.size(); i1++) { -+ final Map.Entry entry = positions.get(i1); -+ Target target = entry.getKey(); - if (node.distanceManhattan(target) <= (float)distance) { - target.setReached(); -- set3.add(target); -+ entryList.add(entry); -+ // Paper end - Perf: remove streams and optimize collection - } - } - -- if (!set3.isEmpty()) { -+ if (!entryList.isEmpty()) { // Paper - Perf: remove streams and optimize collection; rename - break; - } - -@@ -91,7 +100,7 @@ public class PathFinder { - if (node2.walkedDistance < followRange && (!node2.inOpenSet() || g < node2.g)) { - node2.cameFrom = node; - node2.g = g; -- node2.h = this.getBestH(node2, set) * 1.5F; -+ node2.h = this.getBestH(node2, positions) * 1.5F; // Paper - Perf: remove streams and optimize collection - if (node2.inOpenSet()) { - this.openSet.changeCost(node2, node2.g + node2.h); - } else { -@@ -103,23 +112,32 @@ public class PathFinder { - } - } - -- Optional optional = !set3.isEmpty() -- ? set3.stream().map(node -> this.reconstructPath(node.getBestNode(), positions.get(node), true)).min(Comparator.comparingInt(Path::getNodeCount)) -- : set.stream() -- .map(targetx -> this.reconstructPath(targetx.getBestNode(), positions.get(targetx), false)) -- .min(Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount)); -+ // Paper start - Perf: remove streams and optimize collection -+ Path best = null; -+ boolean entryListIsEmpty = entryList.isEmpty(); -+ Comparator comparator = entryListIsEmpty ? Comparator.comparingInt(Path::getNodeCount) -+ : Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount); -+ for (Map.Entry entry : entryListIsEmpty ? positions : entryList) { -+ Path path = this.reconstructPath(entry.getKey().getBestNode(), entry.getValue(), !entryListIsEmpty); -+ if (best == null || comparator.compare(path, best) < 0) -+ best = path; -+ } - profiler.pop(); -- return optional.isEmpty() ? null : optional.get(); -+ return best; -+ // Paper end - Perf: remove streams and optimize collection - } - - protected float distance(Node a, Node b) { - return a.distanceTo(b); - } - -- private float getBestH(Node node, Set targets) { -+ private float getBestH(Node node, List> targets) { // Paper - Perf: remove streams and optimize collection; Set -> List> - float f = Float.MAX_VALUE; - -- for (Target target : targets) { -+ // Paper start - Perf: remove streams and optimize collection -+ for (int i = 0, targetsSize = targets.size(); i < targetsSize; i++) { -+ final Target target = targets.get(i).getKey(); -+ // Paper end - Perf: remove streams and optimize collection - float g = node.distanceTo(target); - target.updateBest(g, node); - f = Math.min(g, f); diff --git a/patches/server/1013-Remove-streams-from-hot-code.patch b/patches/server/1013-Remove-streams-from-hot-code.patch new file mode 100644 index 0000000000..4c6d94f7bf --- /dev/null +++ b/patches/server/1013-Remove-streams-from-hot-code.patch @@ -0,0 +1,215 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: JRoy +Date: Wed, 1 Jul 2020 18:01:49 -0400 +Subject: [PATCH] Remove streams from hot code + +Co-authored-by: Bjarne Koll +Co-authored-by: Spottedleaf + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +index 6a270c9adb044a6e0b1c8e09b9106d51989fd761..d4581127366736c54f74e4ef7479236b18fb487d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +@@ -57,7 +57,7 @@ public class GateBehavior implements BehaviorControl + if (this.hasRequiredMemories(entity)) { + this.status = Behavior.Status.RUNNING; + this.orderPolicy.apply(this.behaviors); +- this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); ++ this.runningPolicy.apply(this.behaviors, world, entity, time); // Paper - Perf: Remove streams from hot code + return true; + } else { + return false; +@@ -66,7 +66,13 @@ public class GateBehavior implements BehaviorControl + + @Override + public final void tickOrStop(ServerLevel world, E entity, long time) { +- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.tickOrStop(world, entity, time)); ++ // Paper start - Perf: Remove streams from hot code ++ for (final BehaviorControl task : this.behaviors) { ++ if (task.getStatus() == Behavior.Status.RUNNING) { ++ task.tickOrStop(world, entity, time); ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + if (this.behaviors.stream().noneMatch(task -> task.getStatus() == Behavior.Status.RUNNING)) { + this.doStop(world, entity, time); + } +@@ -75,8 +81,16 @@ public class GateBehavior implements BehaviorControl + @Override + public final void doStop(ServerLevel world, E entity, long time) { + this.status = Behavior.Status.STOPPED; +- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.doStop(world, entity, time)); +- this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); ++ // Paper start - Perf: Remove streams from hot code ++ for (final BehaviorControl task : this.behaviors) { ++ if (task.getStatus() == Behavior.Status.RUNNING) { ++ task.doStop(world, entity, time); ++ } ++ } ++ for (final MemoryModuleType exitErasedMemory : this.exitErasedMemories) { ++ entity.getBrain().eraseMemory(exitErasedMemory); ++ } ++ // Paper end - Perf: Remove streams from hot code + } + + @Override +@@ -111,18 +125,30 @@ public class GateBehavior implements BehaviorControl + + public static enum RunningPolicy { + RUN_ONE { ++ // Paper start - Perf: Remove streams from hot code + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).filter(task -> task.tryStart(world, entity, time)).findFirst(); ++ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { ++ for (final BehaviorControl task : tasks) { ++ if (task.getStatus() == Behavior.Status.STOPPED && task.tryStart(world, entity, time)) { ++ break; ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + } + }, + TRY_ALL { ++ // Paper start - Perf: Remove streams from hot code + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).forEach(task -> task.tryStart(world, entity, time)); ++ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { ++ for (final BehaviorControl task : tasks) { ++ if (task.getStatus() == Behavior.Status.STOPPED) { ++ task.tryStart(world, entity, time); ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + } + }; + +- public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); ++ public abstract void apply(ShufflingList> tasks, ServerLevel world, E entity, long time); // Paper - Perf: Remove streams from hot code + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java +index aa32804bc9affe9a615d3ffaa513f6f09aab3f32..c7f012674361a323c1efeca4660cd3f46d308ee1 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java ++++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java +@@ -59,8 +59,22 @@ public class GossipContainer { + return this.gossips.entrySet().stream().flatMap(entry -> entry.getValue().unpack(entry.getKey())); + } + ++ // Paper start - Perf: Remove streams from hot code ++ private List decompress() { ++ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ for (Map.Entry entry : this.gossips.entrySet()) { ++ for (GossipContainer.GossipEntry cur : entry.getValue().decompress(entry.getKey())) { ++ if (cur.weightedValue() != 0) { ++ list.add(cur); ++ } ++ } ++ } ++ return list; ++ } ++ // Paper end - Perf: Remove streams from hot code ++ + private Collection selectGossipsForTransfer(RandomSource random, int count) { +- List list = this.unpack().toList(); ++ List list = this.decompress(); // Paper - Perf: Remove streams from hot code + if (list.isEmpty()) { + return Collections.emptyList(); + } else { +@@ -145,7 +159,7 @@ public class GossipContainer { + + public T store(DynamicOps ops) { + return GossipContainer.GossipEntry.LIST_CODEC +- .encodeStart(ops, this.unpack().toList()) ++ .encodeStart(ops, this.decompress()) // Paper - Perf: Remove streams from hot code + .resultOrPartial(error -> LOGGER.warn("Failed to serialize gossips: {}", error)) + .orElseGet(ops::emptyList); + } +@@ -172,12 +186,23 @@ public class GossipContainer { + final Object2IntMap entries = new Object2IntOpenHashMap<>(); + + public int weightedValue(Predicate gossipTypeFilter) { +- return this.entries +- .object2IntEntrySet() +- .stream() +- .filter(entry -> gossipTypeFilter.test(entry.getKey())) +- .mapToInt(entry -> entry.getIntValue() * entry.getKey().weight) +- .sum(); ++ // Paper start - Perf: Remove streams from hot code ++ int weight = 0; ++ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { ++ if (gossipTypeFilter.test(entry.getKey())) { ++ weight += entry.getIntValue() * entry.getKey().weight; ++ } ++ } ++ return weight; ++ } ++ ++ public List decompress(UUID uuid) { ++ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { ++ list.add(new GossipContainer.GossipEntry(uuid, entry.getKey(), entry.getIntValue())); ++ } ++ return list; ++ // Paper end - Perf: Remove streams from hot code + } + + public Stream unpack(UUID target) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +index 28fd073ddad358e087e8c78985a97cad21be81b7..a5bd308d1b3ea5db185c06a287167d1d8894a987 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +@@ -24,13 +24,17 @@ public class NearestItemSensor extends Sensor { + @Override + protected void doTick(ServerLevel world, Mob entity) { + Brain brain = entity.getBrain(); +- List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true); ++ List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> itemEntity.closerThan(entity, MAX_DISTANCE_TO_WANTED_ITEM) && entity.wantsToPickUp(itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities + list.sort(Comparator.comparingDouble(entity::distanceToSqr)); +- Optional optional = list.stream() +- .filter(itemEntity -> entity.wantsToPickUp(itemEntity.getItem())) +- .filter(itemEntity -> itemEntity.closerThan(entity, 32.0)) +- .filter(entity::hasLineOfSight) +- .findFirst(); +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional); ++ // Paper start - Perf: remove streams from hot code ++ ItemEntity nearest = null; ++ for (ItemEntity entityItem : list) { ++ if (entity.hasLineOfSight(entityItem)) { // Paper - Perf: Move predicate into getEntities ++ nearest = entityItem; ++ break; ++ } ++ } ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); ++ // Paper end - Perf: remove streams from hot code + } + } +diff --git a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java +index 04552e2cf3de2dc41dfe7e7a2e88b200eaaf280b..ca93a97256350789ca56f910862c9d717ca7670b 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java +@@ -35,9 +35,10 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { + int j = pos.getMinBlockZ(); + ObjectList objectList = new ObjectArrayList<>(10); + ObjectList objectList2 = new ObjectArrayList<>(32); +- world.startsForStructure(pos, structure -> structure.terrainAdaptation() != TerrainAdjustment.NONE) +- .forEach( +- start -> { ++ // Paper start - Perf: Remove streams from hot code ++ for (net.minecraft.world.level.levelgen.structure.StructureStart start : world.startsForStructure(pos, (structure) -> { ++ return structure.terrainAdaptation() != TerrainAdjustment.NONE; ++ })) { // Paper end - Perf: Remove streams from hot code + TerrainAdjustment terrainAdjustment = start.getStructure().terrainAdaptation(); + + for (StructurePiece structurePiece : start.getPieces()) { +@@ -65,8 +66,7 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { + } + } + } +- } +- ); ++ } // Paper - Perf: Remove streams from hot code + return new Beardifier(objectList.iterator(), objectList2.iterator()); + } + diff --git a/patches/server/1014-Custom-table-implementation-for-blockstate-state-loo.patch b/patches/server/1014-Custom-table-implementation-for-blockstate-state-loo.patch deleted file mode 100644 index 96d90abea3..0000000000 --- a/patches/server/1014-Custom-table-implementation-for-blockstate-state-loo.patch +++ /dev/null @@ -1,343 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 11 Mar 2021 20:05:44 -0800 -Subject: [PATCH] Custom table implementation for blockstate state lookups - -Testing some redstone intensive machines showed to bring about a 10% -improvement. - -diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java -new file mode 100644 -index 0000000000000000000000000000000000000000..57d0cd3ad6f972e986c72a57f1a6e36003f190c2 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java -@@ -0,0 +1,160 @@ -+package io.papermc.paper.util.table; -+ -+import com.google.common.collect.Table; -+import net.minecraft.world.level.block.state.StateHolder; -+import net.minecraft.world.level.block.state.properties.Property; -+import java.util.Collection; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class ZeroCollidingReferenceStateTable { -+ -+ // upper 32 bits: starting index -+ // lower 32 bits: bitset for contained ids -+ protected final long[] this_index_table; -+ protected final Comparable[] this_table; -+ protected final StateHolder this_state; -+ -+ protected long[] index_table; -+ protected StateHolder[][] value_table; -+ -+ public ZeroCollidingReferenceStateTable(final StateHolder state, final Map, Comparable> this_map) { -+ this.this_state = state; -+ this.this_index_table = this.create_table(this_map.keySet()); -+ -+ int max_id = -1; -+ for (final Property property : this_map.keySet()) { -+ final int id = lookup_vindex(property, this.this_index_table); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ this.this_table = new Comparable[max_id + 1]; -+ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { -+ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); -+ } -+ } -+ -+ public void loadInTable(final Table, Comparable, StateHolder> table, -+ final Map, Comparable> this_map) { -+ final Set> combined = new HashSet<>(table.rowKeySet()); -+ combined.addAll(this_map.keySet()); -+ -+ this.index_table = this.create_table(combined); -+ -+ int max_id = -1; -+ for (final Property property : combined) { -+ final int id = lookup_vindex(property, this.index_table); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ this.value_table = new StateHolder[max_id + 1][]; -+ -+ final Map, Map, StateHolder>> map = table.rowMap(); -+ for (final Property property : map.keySet()) { -+ final Map, StateHolder> propertyMap = map.get(property); -+ -+ final int id = lookup_vindex(property, this.index_table); -+ final StateHolder[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; -+ -+ for (final Map.Entry, StateHolder> entry : propertyMap.entrySet()) { -+ if (entry.getValue() == null) { -+ // TODO what -+ continue; -+ } -+ -+ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); -+ } -+ } -+ -+ -+ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { -+ final Property property = entry.getKey(); -+ final int index = lookup_vindex(property, this.index_table); -+ -+ if (this.value_table[index] == null) { -+ this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; -+ } -+ -+ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; -+ } -+ } -+ -+ -+ protected long[] create_table(final Collection> collection) { -+ int max_id = -1; -+ for (final Property property : collection) { -+ final int id = property.getId(); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) -+ -+ for (final Property property : collection) { -+ final int id = property.getId(); -+ -+ ret[id >>> 5] |= (1L << (id & 31)); -+ } -+ -+ int total = 0; -+ for (int i = 1, len = ret.length; i < len; ++i) { -+ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; -+ } -+ -+ return ret; -+ } -+ -+ public Comparable get(final Property state) { -+ final Comparable[] table = this.this_table; -+ final int index = lookup_vindex(state, this.this_index_table); -+ -+ if (index < 0 || index >= table.length) { -+ return null; -+ } -+ return table[index]; -+ } -+ -+ public StateHolder get(final Property property, final Comparable with) { -+ final int withId = ((Property)property).getIdFor(with); -+ if (withId < 0) { -+ return null; -+ } -+ -+ final int index = lookup_vindex(property, this.index_table); -+ final StateHolder[][] table = this.value_table; -+ if (index < 0 || index >= table.length) { -+ return null; -+ } -+ -+ final StateHolder[] values = table[index]; -+ -+ if (withId >= values.length) { -+ return null; -+ } -+ -+ return values[withId]; -+ } -+ -+ protected static int lookup_vindex(final Property property, final long[] index_table) { -+ final int id = property.getId(); -+ final long bitset_mask = (1L << (id & 31)); -+ final long lower_mask = bitset_mask - 1; -+ final int index = id >>> 5; -+ if (index >= index_table.length) { -+ return -1; -+ } -+ final long index_value = index_table[index]; -+ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain -+ -+ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id -+ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, -+ // otherwise it comes out as -1. -+ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); -+ } -+} -diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -index daedcfd867ed6171fb61bdcbded417a11c8a5b0f..45744d86e9582a93a0cec26009deea091080fbbe 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -+++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -@@ -39,11 +39,13 @@ public abstract class StateHolder { - private final Reference2ObjectArrayMap, Comparable> values; - private Table, Comparable, S> neighbours; - protected final MapCodec propertiesCodec; -+ protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup - - protected StateHolder(O owner, Reference2ObjectArrayMap, Comparable> propertyMap, MapCodec codec) { - this.owner = owner; - this.values = propertyMap; - this.propertiesCodec = codec; -+ this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup - } - - public > S cycle(Property property) { -@@ -84,11 +86,11 @@ public abstract class StateHolder { - } - - public > boolean hasProperty(Property property) { -- return this.values.containsKey(property); -+ return this.optimisedTable.get(property) != null; // Paper - optimise state lookup - } - - public > T getValue(Property property) { -- Comparable comparable = this.values.get(property); -+ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup - if (comparable == null) { - throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); - } else { -@@ -97,24 +99,18 @@ public abstract class StateHolder { - } - - public > Optional getOptionalValue(Property property) { -- Comparable comparable = this.values.get(property); -+ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup - return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); - } - - public , V extends T> S setValue(Property property, V value) { -- Comparable comparable = this.values.get(property); -- if (comparable == null) { -- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); -- } else if (comparable.equals(value)) { -- return (S)this; -- } else { -- S object = this.neighbours.get(property, value); -- if (object == null) { -- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); -- } else { -- return object; -- } -+ // Paper start - optimise state lookup -+ final S ret = (S)this.optimisedTable.get(property, value); -+ if (ret == null) { -+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); - } -+ return ret; -+ // Paper end - optimise state lookup - } - - public , V extends T> S trySetValue(Property property, V value) { -@@ -147,7 +143,7 @@ public abstract class StateHolder { - } - } - -- this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); -+ this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup - } - } - -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -index b63116b333b6e06494091a82588acfb639bddb71..ff5fd91257c4554c523682009efe1db83f53fd5b 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -@@ -7,6 +7,13 @@ import java.util.Optional; - public class BooleanProperty extends Property { - private final ImmutableSet values = ImmutableSet.of(true, false); - -+ // Paper start - optimise iblockdata state lookup -+ @Override -+ public final int getIdFor(final Boolean value) { -+ return value.booleanValue() ? 1 : 0; -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected BooleanProperty(String name) { - super(name, Boolean.class); - } -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -index 3097298fe356df98967cf4bdeaaede69dfe8a441..498c5abe0a9d024d77029719c621c1c8485791f3 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -@@ -15,6 +15,15 @@ public class EnumProperty & StringRepresentable> extends Prope - private final ImmutableSet values; - private final Map names = Maps.newHashMap(); - -+ // Paper start - optimise iblockdata state lookup -+ private int[] idLookupTable; -+ -+ @Override -+ public final int getIdFor(final T value) { -+ return this.idLookupTable[value.ordinal()]; -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected EnumProperty(String name, Class type, Collection values) { - super(name, type); - this.values = ImmutableSet.copyOf(values); -@@ -27,6 +36,14 @@ public class EnumProperty & StringRepresentable> extends Prope - - this.names.put(string, enum_); - } -+ // Paper start - optimise BlockState lookup -+ int id = 0; -+ this.idLookupTable = new int[type.getEnumConstants().length]; -+ java.util.Arrays.fill(this.idLookupTable, -1); -+ for (final T value : this.getPossibleValues()) { -+ this.idLookupTable[value.ordinal()] = id++; -+ } -+ // Paper end - optimise BlockState lookup - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -index 3a850321a4bcc68058483b5fd53e829c425a68af..977504f2641d0133a572b0d5de85d058609343bb 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -@@ -11,6 +11,16 @@ public class IntegerProperty extends Property { - public final int min; - public final int max; - -+ // Paper start - optimise iblockdata state lookup -+ @Override -+ public final int getIdFor(final Integer value) { -+ final int val = value.intValue(); -+ final int ret = val - this.min; -+ -+ return ret | ((this.max - ret) >> 31); -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected IntegerProperty(String name, int min, int max) { - super(name, Integer.class); - if (min < 0) { -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -index 9055f15af0cae55effa6942913a9d7edf3857e07..b9493e3762410aca8e683c32b5aef187c0bee082 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -@@ -24,6 +24,17 @@ public abstract class Property> { - ); - private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); - -+ // Paper start - optimise iblockdata state lookup -+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); -+ private final int id = ID_GENERATOR.getAndIncrement(); -+ -+ public final int getId() { -+ return this.id; -+ } -+ -+ public abstract int getIdFor(final T value); -+ // Paper end - optimise state lookup -+ - protected Property(String name, Class type) { - this.clazz = type; - this.name = name; diff --git a/patches/server/1014-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch b/patches/server/1014-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch new file mode 100644 index 0000000000..6fc4445bbf --- /dev/null +++ b/patches/server/1014-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch @@ -0,0 +1,133 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Tue, 4 Aug 2020 22:24:15 +0200 +Subject: [PATCH] Optimize Pathfinder - Remove Streams / Optimized collections + +I utilized the IDE to convert streams to non streams code, so shouldn't +be any risk of behavior change. Only did minor optimization of the +generated code set to remove unnecessary things. + +I expect us to just drop this patch on next major update and re-apply +it with the IDE again and re-apply the collections optimization. + +Optimize collection by creating a list instead of a set of the key and value. + +This lets us get faster foreach iteration, as well as avoids map lookups on +the values when needed. + +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +index 8a483c8d2a54617d78af19de32fa0ded71b3223a..18bbb3f8f99849333ff4bc020c8ce758a69312a5 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -38,8 +38,12 @@ public class PathFinder { + if (node == null) { + return null; + } else { +- Map map = positions.stream() +- .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget((double)pos.getX(), (double)pos.getY(), (double)pos.getZ()), Function.identity())); ++ // Paper start - Perf: remove streams and optimize collection ++ List> map = Lists.newArrayList(); ++ for (final BlockPos pos : positions) { ++ map.add(new java.util.AbstractMap.SimpleEntry<>(this.nodeEvaluator.getTarget(pos.getX(), pos.getY(), pos.getZ()), pos)); ++ } ++ // Paper end - Perf: remove streams and optimize collection + Path path = this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); + this.nodeEvaluator.done(); + return path; +@@ -47,18 +51,19 @@ public class PathFinder { + } + + @Nullable +- private Path findPath(ProfilerFiller profiler, Node startNode, Map positions, float followRange, int distance, float rangeMultiplier) { ++ // Paper start - Perf: remove streams and optimize collection ++ private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { + profiler.push("find_path"); + profiler.markForCharting(MetricCategory.PATH_FINDING); +- Set set = positions.keySet(); ++ // Set set = positions.keySet(); + startNode.g = 0.0F; +- startNode.h = this.getBestH(startNode, set); ++ startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection + startNode.f = startNode.h; + this.openSet.clear(); + this.openSet.insert(startNode); +- Set set2 = ImmutableSet.of(); ++ // Set set2 = ImmutableSet.of(); // Paper - unused - diff on change + int i = 0; +- Set set3 = Sets.newHashSetWithExpectedSize(set.size()); ++ List> entryList = Lists.newArrayListWithExpectedSize(positions.size()); // Paper - optimize collection + int j = (int)((float)this.maxVisitedNodes * rangeMultiplier); + + while (!this.openSet.isEmpty()) { +@@ -69,14 +74,18 @@ public class PathFinder { + Node node = this.openSet.pop(); + node.closed = true; + +- for (Target target : set) { ++ // Paper start - optimize collection ++ for (int i1 = 0; i1 < positions.size(); i1++) { ++ final Map.Entry entry = positions.get(i1); ++ Target target = entry.getKey(); + if (node.distanceManhattan(target) <= (float)distance) { + target.setReached(); +- set3.add(target); ++ entryList.add(entry); ++ // Paper end - Perf: remove streams and optimize collection + } + } + +- if (!set3.isEmpty()) { ++ if (!entryList.isEmpty()) { // Paper - Perf: remove streams and optimize collection; rename + break; + } + +@@ -91,7 +100,7 @@ public class PathFinder { + if (node2.walkedDistance < followRange && (!node2.inOpenSet() || g < node2.g)) { + node2.cameFrom = node; + node2.g = g; +- node2.h = this.getBestH(node2, set) * 1.5F; ++ node2.h = this.getBestH(node2, positions) * 1.5F; // Paper - Perf: remove streams and optimize collection + if (node2.inOpenSet()) { + this.openSet.changeCost(node2, node2.g + node2.h); + } else { +@@ -103,23 +112,32 @@ public class PathFinder { + } + } + +- Optional optional = !set3.isEmpty() +- ? set3.stream().map(node -> this.reconstructPath(node.getBestNode(), positions.get(node), true)).min(Comparator.comparingInt(Path::getNodeCount)) +- : set.stream() +- .map(targetx -> this.reconstructPath(targetx.getBestNode(), positions.get(targetx), false)) +- .min(Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount)); ++ // Paper start - Perf: remove streams and optimize collection ++ Path best = null; ++ boolean entryListIsEmpty = entryList.isEmpty(); ++ Comparator comparator = entryListIsEmpty ? Comparator.comparingInt(Path::getNodeCount) ++ : Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount); ++ for (Map.Entry entry : entryListIsEmpty ? positions : entryList) { ++ Path path = this.reconstructPath(entry.getKey().getBestNode(), entry.getValue(), !entryListIsEmpty); ++ if (best == null || comparator.compare(path, best) < 0) ++ best = path; ++ } + profiler.pop(); +- return optional.isEmpty() ? null : optional.get(); ++ return best; ++ // Paper end - Perf: remove streams and optimize collection + } + + protected float distance(Node a, Node b) { + return a.distanceTo(b); + } + +- private float getBestH(Node node, Set targets) { ++ private float getBestH(Node node, List> targets) { // Paper - Perf: remove streams and optimize collection; Set -> List> + float f = Float.MAX_VALUE; + +- for (Target target : targets) { ++ // Paper start - Perf: remove streams and optimize collection ++ for (int i = 0, targetsSize = targets.size(); i < targetsSize; i++) { ++ final Target target = targets.get(i).getKey(); ++ // Paper end - Perf: remove streams and optimize collection + float g = node.distanceTo(target); + target.updateBest(g, node); + f = Math.min(g, f); diff --git a/patches/server/1015-Custom-table-implementation-for-blockstate-state-loo.patch b/patches/server/1015-Custom-table-implementation-for-blockstate-state-loo.patch new file mode 100644 index 0000000000..96d90abea3 --- /dev/null +++ b/patches/server/1015-Custom-table-implementation-for-blockstate-state-loo.patch @@ -0,0 +1,343 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 11 Mar 2021 20:05:44 -0800 +Subject: [PATCH] Custom table implementation for blockstate state lookups + +Testing some redstone intensive machines showed to bring about a 10% +improvement. + +diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57d0cd3ad6f972e986c72a57f1a6e36003f190c2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java +@@ -0,0 +1,160 @@ ++package io.papermc.paper.util.table; ++ ++import com.google.common.collect.Table; ++import net.minecraft.world.level.block.state.StateHolder; ++import net.minecraft.world.level.block.state.properties.Property; ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class ZeroCollidingReferenceStateTable { ++ ++ // upper 32 bits: starting index ++ // lower 32 bits: bitset for contained ids ++ protected final long[] this_index_table; ++ protected final Comparable[] this_table; ++ protected final StateHolder this_state; ++ ++ protected long[] index_table; ++ protected StateHolder[][] value_table; ++ ++ public ZeroCollidingReferenceStateTable(final StateHolder state, final Map, Comparable> this_map) { ++ this.this_state = state; ++ this.this_index_table = this.create_table(this_map.keySet()); ++ ++ int max_id = -1; ++ for (final Property property : this_map.keySet()) { ++ final int id = lookup_vindex(property, this.this_index_table); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ this.this_table = new Comparable[max_id + 1]; ++ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { ++ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); ++ } ++ } ++ ++ public void loadInTable(final Table, Comparable, StateHolder> table, ++ final Map, Comparable> this_map) { ++ final Set> combined = new HashSet<>(table.rowKeySet()); ++ combined.addAll(this_map.keySet()); ++ ++ this.index_table = this.create_table(combined); ++ ++ int max_id = -1; ++ for (final Property property : combined) { ++ final int id = lookup_vindex(property, this.index_table); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ this.value_table = new StateHolder[max_id + 1][]; ++ ++ final Map, Map, StateHolder>> map = table.rowMap(); ++ for (final Property property : map.keySet()) { ++ final Map, StateHolder> propertyMap = map.get(property); ++ ++ final int id = lookup_vindex(property, this.index_table); ++ final StateHolder[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; ++ ++ for (final Map.Entry, StateHolder> entry : propertyMap.entrySet()) { ++ if (entry.getValue() == null) { ++ // TODO what ++ continue; ++ } ++ ++ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); ++ } ++ } ++ ++ ++ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { ++ final Property property = entry.getKey(); ++ final int index = lookup_vindex(property, this.index_table); ++ ++ if (this.value_table[index] == null) { ++ this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; ++ } ++ ++ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; ++ } ++ } ++ ++ ++ protected long[] create_table(final Collection> collection) { ++ int max_id = -1; ++ for (final Property property : collection) { ++ final int id = property.getId(); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) ++ ++ for (final Property property : collection) { ++ final int id = property.getId(); ++ ++ ret[id >>> 5] |= (1L << (id & 31)); ++ } ++ ++ int total = 0; ++ for (int i = 1, len = ret.length; i < len; ++i) { ++ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; ++ } ++ ++ return ret; ++ } ++ ++ public Comparable get(final Property state) { ++ final Comparable[] table = this.this_table; ++ final int index = lookup_vindex(state, this.this_index_table); ++ ++ if (index < 0 || index >= table.length) { ++ return null; ++ } ++ return table[index]; ++ } ++ ++ public StateHolder get(final Property property, final Comparable with) { ++ final int withId = ((Property)property).getIdFor(with); ++ if (withId < 0) { ++ return null; ++ } ++ ++ final int index = lookup_vindex(property, this.index_table); ++ final StateHolder[][] table = this.value_table; ++ if (index < 0 || index >= table.length) { ++ return null; ++ } ++ ++ final StateHolder[] values = table[index]; ++ ++ if (withId >= values.length) { ++ return null; ++ } ++ ++ return values[withId]; ++ } ++ ++ protected static int lookup_vindex(final Property property, final long[] index_table) { ++ final int id = property.getId(); ++ final long bitset_mask = (1L << (id & 31)); ++ final long lower_mask = bitset_mask - 1; ++ final int index = id >>> 5; ++ if (index >= index_table.length) { ++ return -1; ++ } ++ final long index_value = index_table[index]; ++ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain ++ ++ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id ++ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, ++ // otherwise it comes out as -1. ++ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); ++ } ++} +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index daedcfd867ed6171fb61bdcbded417a11c8a5b0f..45744d86e9582a93a0cec26009deea091080fbbe 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -39,11 +39,13 @@ public abstract class StateHolder { + private final Reference2ObjectArrayMap, Comparable> values; + private Table, Comparable, S> neighbours; + protected final MapCodec propertiesCodec; ++ protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup + + protected StateHolder(O owner, Reference2ObjectArrayMap, Comparable> propertyMap, MapCodec codec) { + this.owner = owner; + this.values = propertyMap; + this.propertiesCodec = codec; ++ this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup + } + + public > S cycle(Property property) { +@@ -84,11 +86,11 @@ public abstract class StateHolder { + } + + public > boolean hasProperty(Property property) { +- return this.values.containsKey(property); ++ return this.optimisedTable.get(property) != null; // Paper - optimise state lookup + } + + public > T getValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup + if (comparable == null) { + throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); + } else { +@@ -97,24 +99,18 @@ public abstract class StateHolder { + } + + public > Optional getOptionalValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup + return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); + } + + public , V extends T> S setValue(Property property, V value) { +- Comparable comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); +- } else if (comparable.equals(value)) { +- return (S)this; +- } else { +- S object = this.neighbours.get(property, value); +- if (object == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); +- } else { +- return object; +- } ++ // Paper start - optimise state lookup ++ final S ret = (S)this.optimisedTable.get(property, value); ++ if (ret == null) { ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); + } ++ return ret; ++ // Paper end - optimise state lookup + } + + public , V extends T> S trySetValue(Property property, V value) { +@@ -147,7 +143,7 @@ public abstract class StateHolder { + } + } + +- this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); ++ this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index b63116b333b6e06494091a82588acfb639bddb71..ff5fd91257c4554c523682009efe1db83f53fd5b 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -7,6 +7,13 @@ import java.util.Optional; + public class BooleanProperty extends Property { + private final ImmutableSet values = ImmutableSet.of(true, false); + ++ // Paper start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Boolean value) { ++ return value.booleanValue() ? 1 : 0; ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected BooleanProperty(String name) { + super(name, Boolean.class); + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 3097298fe356df98967cf4bdeaaede69dfe8a441..498c5abe0a9d024d77029719c621c1c8485791f3 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -15,6 +15,15 @@ public class EnumProperty & StringRepresentable> extends Prope + private final ImmutableSet values; + private final Map names = Maps.newHashMap(); + ++ // Paper start - optimise iblockdata state lookup ++ private int[] idLookupTable; ++ ++ @Override ++ public final int getIdFor(final T value) { ++ return this.idLookupTable[value.ordinal()]; ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected EnumProperty(String name, Class type, Collection values) { + super(name, type); + this.values = ImmutableSet.copyOf(values); +@@ -27,6 +36,14 @@ public class EnumProperty & StringRepresentable> extends Prope + + this.names.put(string, enum_); + } ++ // Paper start - optimise BlockState lookup ++ int id = 0; ++ this.idLookupTable = new int[type.getEnumConstants().length]; ++ java.util.Arrays.fill(this.idLookupTable, -1); ++ for (final T value : this.getPossibleValues()) { ++ this.idLookupTable[value.ordinal()] = id++; ++ } ++ // Paper end - optimise BlockState lookup + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 3a850321a4bcc68058483b5fd53e829c425a68af..977504f2641d0133a572b0d5de85d058609343bb 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -11,6 +11,16 @@ public class IntegerProperty extends Property { + public final int min; + public final int max; + ++ // Paper start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Integer value) { ++ final int val = value.intValue(); ++ final int ret = val - this.min; ++ ++ return ret | ((this.max - ret) >> 31); ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + if (min < 0) { +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index 9055f15af0cae55effa6942913a9d7edf3857e07..b9493e3762410aca8e683c32b5aef187c0bee082 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -24,6 +24,17 @@ public abstract class Property> { + ); + private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); + ++ // Paper start - optimise iblockdata state lookup ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id = ID_GENERATOR.getAndIncrement(); ++ ++ public final int getId() { ++ return this.id; ++ } ++ ++ public abstract int getIdFor(final T value); ++ // Paper end - optimise state lookup ++ + protected Property(String name, Class type) { + this.clazz = type; + this.name = name; diff --git a/patches/server/1015-Fix-entity-type-tags-suggestions-in-selectors.patch b/patches/server/1015-Fix-entity-type-tags-suggestions-in-selectors.patch deleted file mode 100644 index a8e41e99e4..0000000000 --- a/patches/server/1015-Fix-entity-type-tags-suggestions-in-selectors.patch +++ /dev/null @@ -1,156 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sun, 22 Aug 2021 15:21:57 -0700 -Subject: [PATCH] Fix entity type tags suggestions in selectors - -This would really be better as a client fix because just to fix it -all EntityArguments have been told to ask the server for completions -when if this was fixed on the client, that wouldn't be needed. - -Mojira Issue: https://bugs.mojang.com/browse/MC-235045 - -diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java -index 4017b82e72fefd6685e9250a936686fd8a0891f1..2d344df35d47b4b1ecddf32ccaa4dae41e5f58cb 100644 ---- a/src/main/java/net/minecraft/commands/CommandSourceStack.java -+++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java -@@ -447,4 +447,20 @@ public class CommandSourceStack implements ExecutionCommandSource getSelectedEntities() { -+ if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && this.source instanceof ServerPlayer player) { -+ final Entity cameraEntity = player.getCamera(); -+ final double pickDistance = player.entityInteractionRange(); -+ final Vec3 min = cameraEntity.getEyePosition(1.0F); -+ final Vec3 viewVector = cameraEntity.getViewVector(1.0F); -+ final Vec3 max = min.add(viewVector.x * pickDistance, viewVector.y * pickDistance, viewVector.z * pickDistance); -+ final net.minecraft.world.phys.AABB aabb = cameraEntity.getBoundingBox().expandTowards(viewVector.scale(pickDistance)).inflate(1.0D, 1.0D, 1.0D); -+ final net.minecraft.world.phys.EntityHitResult hitResult = net.minecraft.world.entity.projectile.ProjectileUtil.getEntityHitResult(cameraEntity, min, max, aabb, (e) -> !e.isSpectator() && e.isPickable(), pickDistance * pickDistance); -+ return hitResult != null ? java.util.Collections.singletonList(hitResult.getEntity().getStringUUID()) : SharedSuggestionProvider.super.getSelectedEntities(); -+ } -+ return SharedSuggestionProvider.super.getSelectedEntities(); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - } -diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java -index e8dcbe7c6d6ed20ad19d2ba1893ad16d917b9993..3e454515360c22a26c9329e4032d525579110d7e 100644 ---- a/src/main/java/net/minecraft/commands/Commands.java -+++ b/src/main/java/net/minecraft/commands/Commands.java -@@ -535,6 +535,7 @@ public class Commands { - resultNodes.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains( ":" )); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below - Iterator iterator = tree.getChildren().iterator(); - -+ boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments - while (iterator.hasNext()) { - CommandNode commandnode2 = (CommandNode) iterator.next(); - // Paper start - Brigadier API -@@ -597,6 +598,12 @@ public class Commands { - - if (requiredargumentbuilder.getSuggestionsProvider() != null) { - requiredargumentbuilder.suggests(SuggestionProviders.safelySwap(requiredargumentbuilder.getSuggestionsProvider())); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ registeredAskServerSuggestionsForTree = requiredargumentbuilder.getSuggestionsProvider() == net.minecraft.commands.synchronization.SuggestionProviders.ASK_SERVER; -+ } else if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && !registeredAskServerSuggestionsForTree && requiredargumentbuilder.getType() instanceof net.minecraft.commands.arguments.EntityArgument) { -+ requiredargumentbuilder.suggests(requiredargumentbuilder.getType()::listSuggestions); -+ registeredAskServerSuggestionsForTree = true; // You can only -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - } - } - -diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -index 2043001c16b3530c2d3f52efda10bcad424881c0..7976885b902a6ce7d80f31e49448c99452eb9765 100644 ---- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -@@ -131,7 +131,7 @@ public class EntityArgument implements ArgumentType { - final boolean permission = object instanceof CommandSourceStack stack - ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") - : icompletionprovider.hasPermission(2); -- EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission); -+ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission, true); // Paper - tell clients to ask server for suggestions for EntityArguments - // Paper end - Fix EntityArgument suggestion permissions - - try { -@@ -141,7 +141,19 @@ public class EntityArgument implements ArgumentType { - } - - return argumentparserselector.fillSuggestions(suggestionsbuilder, (suggestionsbuilder1) -> { -- Collection collection = icompletionprovider.getOnlinePlayerNames(); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ final Collection collection; -+ if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { -+ collection = new java.util.ArrayList<>(); -+ for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { -+ if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { -+ collection.add(player.getGameProfile().getName()); -+ } -+ } -+ } else { -+ collection = icompletionprovider.getOnlinePlayerNames(); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - Iterable iterable = this.playersOnly ? collection : Iterables.concat(collection, icompletionprovider.getSelectedEntities()); - - SharedSuggestionProvider.suggest((Iterable) iterable, suggestionsbuilder1); -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -index 6b6756625f3fb1f36f940c6e31e97b84f32a9990..32b39803380456f40bafdb790eaa27efab5f06a6 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -@@ -115,12 +115,19 @@ public class EntitySelectorParser { - private boolean hasScores; - private boolean hasAdvancements; - private boolean usesSelectors; -+ public boolean parsingEntityArgumentSuggestions; // Paper - tell clients to ask server for suggestions for EntityArguments - - public EntitySelectorParser(StringReader reader) { - this(reader, true); - } - - public EntitySelectorParser(StringReader reader, boolean atAllowed) { -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ this(reader, atAllowed, false); -+ } -+ public EntitySelectorParser(StringReader reader, boolean atAllowed, boolean parsingEntityArgumentSuggestions) { -+ this.parsingEntityArgumentSuggestions = parsingEntityArgumentSuggestions; -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - this.distance = MinMaxBounds.Doubles.ANY; - this.level = MinMaxBounds.Ints.ANY; - this.rotX = WrappedMinMaxBounds.ANY; -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -index 44982ff2cdd6e03f0b9ce8c3cc87561ef183ef06..626fe7a45c2edba68eb201974e9f8f5eebf75cc0 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -@@ -76,6 +76,19 @@ public class EntitySelectorOptions { - public static final DynamicCommandExceptionType ERROR_ENTITY_TYPE_INVALID = new DynamicCommandExceptionType( - entity -> Component.translatableEscape("argument.entity.options.type.invalid", entity) - ); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ public static final DynamicCommandExceptionType ERROR_ENTITY_TAG_INVALID = new DynamicCommandExceptionType((object) -> { -+ return io.papermc.paper.adventure.PaperAdventure -+ .asVanilla(net.kyori.adventure.text.Component -+ .text("Invalid or unknown entity type tag '" + object + "'") -+ .hoverEvent(net.kyori.adventure.text.event.HoverEvent -+ .showText(net.kyori.adventure.text.Component -+ .text("You can disable this error in 'paper.yml'") -+ ) -+ ) -+ ); -+ }); -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - - private static void register(String id, EntitySelectorOptions.Modifier handler, Predicate condition, Component description) { - OPTIONS.put(id, new EntitySelectorOptions.Option(handler, condition, description)); -@@ -297,6 +310,12 @@ public class EntitySelectorOptions { - - if (reader.isTag()) { - TagKey> tagKey = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.read(reader.getReader())); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments; throw error if invalid entity tag (only on suggestions to keep cmd success behavior) -+ if (reader.parsingEntityArgumentSuggestions && io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getTag(tagKey).isEmpty()) { -+ reader.getReader().setCursor(i); -+ throw ERROR_ENTITY_TAG_INVALID.createWithContext(reader.getReader(), tagKey); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - reader.addPredicate(entity -> entity.getType().is(tagKey) != bl); - } else { - ResourceLocation resourceLocation = ResourceLocation.read(reader.getReader()); diff --git a/patches/server/1016-Fix-entity-type-tags-suggestions-in-selectors.patch b/patches/server/1016-Fix-entity-type-tags-suggestions-in-selectors.patch new file mode 100644 index 0000000000..a8e41e99e4 --- /dev/null +++ b/patches/server/1016-Fix-entity-type-tags-suggestions-in-selectors.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 22 Aug 2021 15:21:57 -0700 +Subject: [PATCH] Fix entity type tags suggestions in selectors + +This would really be better as a client fix because just to fix it +all EntityArguments have been told to ask the server for completions +when if this was fixed on the client, that wouldn't be needed. + +Mojira Issue: https://bugs.mojang.com/browse/MC-235045 + +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index 4017b82e72fefd6685e9250a936686fd8a0891f1..2d344df35d47b4b1ecddf32ccaa4dae41e5f58cb 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -447,4 +447,20 @@ public class CommandSourceStack implements ExecutionCommandSource getSelectedEntities() { ++ if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && this.source instanceof ServerPlayer player) { ++ final Entity cameraEntity = player.getCamera(); ++ final double pickDistance = player.entityInteractionRange(); ++ final Vec3 min = cameraEntity.getEyePosition(1.0F); ++ final Vec3 viewVector = cameraEntity.getViewVector(1.0F); ++ final Vec3 max = min.add(viewVector.x * pickDistance, viewVector.y * pickDistance, viewVector.z * pickDistance); ++ final net.minecraft.world.phys.AABB aabb = cameraEntity.getBoundingBox().expandTowards(viewVector.scale(pickDistance)).inflate(1.0D, 1.0D, 1.0D); ++ final net.minecraft.world.phys.EntityHitResult hitResult = net.minecraft.world.entity.projectile.ProjectileUtil.getEntityHitResult(cameraEntity, min, max, aabb, (e) -> !e.isSpectator() && e.isPickable(), pickDistance * pickDistance); ++ return hitResult != null ? java.util.Collections.singletonList(hitResult.getEntity().getStringUUID()) : SharedSuggestionProvider.super.getSelectedEntities(); ++ } ++ return SharedSuggestionProvider.super.getSelectedEntities(); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + } +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index e8dcbe7c6d6ed20ad19d2ba1893ad16d917b9993..3e454515360c22a26c9329e4032d525579110d7e 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -535,6 +535,7 @@ public class Commands { + resultNodes.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains( ":" )); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below + Iterator iterator = tree.getChildren().iterator(); + ++ boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments + while (iterator.hasNext()) { + CommandNode commandnode2 = (CommandNode) iterator.next(); + // Paper start - Brigadier API +@@ -597,6 +598,12 @@ public class Commands { + + if (requiredargumentbuilder.getSuggestionsProvider() != null) { + requiredargumentbuilder.suggests(SuggestionProviders.safelySwap(requiredargumentbuilder.getSuggestionsProvider())); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ registeredAskServerSuggestionsForTree = requiredargumentbuilder.getSuggestionsProvider() == net.minecraft.commands.synchronization.SuggestionProviders.ASK_SERVER; ++ } else if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && !registeredAskServerSuggestionsForTree && requiredargumentbuilder.getType() instanceof net.minecraft.commands.arguments.EntityArgument) { ++ requiredargumentbuilder.suggests(requiredargumentbuilder.getType()::listSuggestions); ++ registeredAskServerSuggestionsForTree = true; // You can only ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + } + } + +diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java +index 2043001c16b3530c2d3f52efda10bcad424881c0..7976885b902a6ce7d80f31e49448c99452eb9765 100644 +--- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java ++++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java +@@ -131,7 +131,7 @@ public class EntityArgument implements ArgumentType { + final boolean permission = object instanceof CommandSourceStack stack + ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") + : icompletionprovider.hasPermission(2); +- EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission); ++ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission, true); // Paper - tell clients to ask server for suggestions for EntityArguments + // Paper end - Fix EntityArgument suggestion permissions + + try { +@@ -141,7 +141,19 @@ public class EntityArgument implements ArgumentType { + } + + return argumentparserselector.fillSuggestions(suggestionsbuilder, (suggestionsbuilder1) -> { +- Collection collection = icompletionprovider.getOnlinePlayerNames(); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ final Collection collection; ++ if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { ++ collection = new java.util.ArrayList<>(); ++ for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { ++ if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { ++ collection.add(player.getGameProfile().getName()); ++ } ++ } ++ } else { ++ collection = icompletionprovider.getOnlinePlayerNames(); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + Iterable iterable = this.playersOnly ? collection : Iterables.concat(collection, icompletionprovider.getSelectedEntities()); + + SharedSuggestionProvider.suggest((Iterable) iterable, suggestionsbuilder1); +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java +index 6b6756625f3fb1f36f940c6e31e97b84f32a9990..32b39803380456f40bafdb790eaa27efab5f06a6 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java +@@ -115,12 +115,19 @@ public class EntitySelectorParser { + private boolean hasScores; + private boolean hasAdvancements; + private boolean usesSelectors; ++ public boolean parsingEntityArgumentSuggestions; // Paper - tell clients to ask server for suggestions for EntityArguments + + public EntitySelectorParser(StringReader reader) { + this(reader, true); + } + + public EntitySelectorParser(StringReader reader, boolean atAllowed) { ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ this(reader, atAllowed, false); ++ } ++ public EntitySelectorParser(StringReader reader, boolean atAllowed, boolean parsingEntityArgumentSuggestions) { ++ this.parsingEntityArgumentSuggestions = parsingEntityArgumentSuggestions; ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + this.distance = MinMaxBounds.Doubles.ANY; + this.level = MinMaxBounds.Ints.ANY; + this.rotX = WrappedMinMaxBounds.ANY; +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java +index 44982ff2cdd6e03f0b9ce8c3cc87561ef183ef06..626fe7a45c2edba68eb201974e9f8f5eebf75cc0 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java +@@ -76,6 +76,19 @@ public class EntitySelectorOptions { + public static final DynamicCommandExceptionType ERROR_ENTITY_TYPE_INVALID = new DynamicCommandExceptionType( + entity -> Component.translatableEscape("argument.entity.options.type.invalid", entity) + ); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ public static final DynamicCommandExceptionType ERROR_ENTITY_TAG_INVALID = new DynamicCommandExceptionType((object) -> { ++ return io.papermc.paper.adventure.PaperAdventure ++ .asVanilla(net.kyori.adventure.text.Component ++ .text("Invalid or unknown entity type tag '" + object + "'") ++ .hoverEvent(net.kyori.adventure.text.event.HoverEvent ++ .showText(net.kyori.adventure.text.Component ++ .text("You can disable this error in 'paper.yml'") ++ ) ++ ) ++ ); ++ }); ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + + private static void register(String id, EntitySelectorOptions.Modifier handler, Predicate condition, Component description) { + OPTIONS.put(id, new EntitySelectorOptions.Option(handler, condition, description)); +@@ -297,6 +310,12 @@ public class EntitySelectorOptions { + + if (reader.isTag()) { + TagKey> tagKey = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.read(reader.getReader())); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments; throw error if invalid entity tag (only on suggestions to keep cmd success behavior) ++ if (reader.parsingEntityArgumentSuggestions && io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getTag(tagKey).isEmpty()) { ++ reader.getReader().setCursor(i); ++ throw ERROR_ENTITY_TAG_INVALID.createWithContext(reader.getReader(), tagKey); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + reader.addPredicate(entity -> entity.getType().is(tagKey) != bl); + } else { + ResourceLocation resourceLocation = ResourceLocation.read(reader.getReader()); diff --git a/patches/server/1016-Handle-Oversized-block-entities-in-chunks.patch b/patches/server/1016-Handle-Oversized-block-entities-in-chunks.patch deleted file mode 100644 index d3283fd63f..0000000000 --- a/patches/server/1016-Handle-Oversized-block-entities-in-chunks.patch +++ /dev/null @@ -1,64 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 6 May 2020 05:00:57 -0400 -Subject: [PATCH] Handle Oversized block entities in chunks - -Splits out Extra Packets if too many TE's are encountered to prevent -creating too large of a packet to sed. - -Co-authored-by: Spottedleaf - -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -index 1e75cd33c32f0e2923681da64b9b73b279933c1b..0a8d07bf68b0ceabd13c70196d357fce79dcc2c3 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -@@ -27,6 +27,14 @@ public class ClientboundLevelChunkPacketData { - private final CompoundTag heightmaps; - private final byte[] buffer; - private final List blockEntitiesData; -+ // Paper start - Handle oversized block entities in chunks -+ private final java.util.List> extraPackets = new java.util.ArrayList<>(); -+ private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750); -+ -+ public List> getExtraPackets() { -+ return this.extraPackets; -+ } -+ // Paper end - Handle oversized block entities in chunks - - // Paper start - Anti-Xray - Add chunk packet info - @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } -@@ -50,8 +58,18 @@ public class ClientboundLevelChunkPacketData { - extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); - // Paper end - this.blockEntitiesData = Lists.newArrayList(); -+ int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks - - for (Entry entry2 : chunk.getBlockEntities().entrySet()) { -+ // Paper start - Handle oversized block entities in chunks -+ if (++totalTileEntities > TE_LIMIT) { -+ var packet = entry2.getValue().getUpdatePacket(); -+ if (packet != null) { -+ this.extraPackets.add(packet); -+ continue; -+ } -+ } -+ // Paper end - Handle oversized block entities in chunks - this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entry2.getValue())); - } - } -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -index 8ead66c134688b11dca15f6509147e726f182e6a..cfcac0fdc130120cb1f8d97c6353d93db7ddf81b 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -@@ -83,4 +83,11 @@ public class ClientboundLevelChunkWithLightPacket implements Packet> getExtraPackets() { -+ return this.chunkData.getExtraPackets(); -+ } -+ // Paper end - Handle oversized block entities in chunks - } diff --git a/patches/server/1017-API-for-checking-sent-chunks.patch b/patches/server/1017-API-for-checking-sent-chunks.patch deleted file mode 100644 index 54f091fb69..0000000000 --- a/patches/server/1017-API-for-checking-sent-chunks.patch +++ /dev/null @@ -1,61 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Flo0 -Date: Mon, 8 Apr 2024 16:43:16 +0200 -Subject: [PATCH] API for checking sent chunks - - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -index 82e8ce73b77accd6a4210f88c9fccb325ae367d4..91c4219b2abf1b5be3dc35f7b8403884e1d36f8d 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -1070,5 +1070,10 @@ public final class RegionizedPlayerChunkLoader { - - // now all tickets should be removed, which is all of our external state - } -+ -+ // For external checks -+ public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() { -+ return this.sentChunks; -+ } - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 1bc343df0e7b8e6e3fadc970a4a4c8d787d93828..7eea190ce8a62960ecc42ff56a4ef71b754184fb 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3483,6 +3483,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - } - // Paper end - -+ // Paper start - Add chunk view API -+ @Override -+ public Set getSentChunkKeys() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( -+ this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().clone() -+ ); -+ } -+ -+ @Override -+ public Set getSentChunks() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw(); -+ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); -+ final org.bukkit.World world = this.getWorld(); -+ -+ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().longIterator(); -+ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); -+ -+ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); -+ } -+ -+ @Override -+ public boolean isChunkSent(final long chunkKey) { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().contains(chunkKey); -+ } -+ // Paper end -+ - public Player.Spigot spigot() - { - return this.spigot; diff --git a/patches/server/1017-Handle-Oversized-block-entities-in-chunks.patch b/patches/server/1017-Handle-Oversized-block-entities-in-chunks.patch new file mode 100644 index 0000000000..d3283fd63f --- /dev/null +++ b/patches/server/1017-Handle-Oversized-block-entities-in-chunks.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 6 May 2020 05:00:57 -0400 +Subject: [PATCH] Handle Oversized block entities in chunks + +Splits out Extra Packets if too many TE's are encountered to prevent +creating too large of a packet to sed. + +Co-authored-by: Spottedleaf + +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +index 1e75cd33c32f0e2923681da64b9b73b279933c1b..0a8d07bf68b0ceabd13c70196d357fce79dcc2c3 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +@@ -27,6 +27,14 @@ public class ClientboundLevelChunkPacketData { + private final CompoundTag heightmaps; + private final byte[] buffer; + private final List blockEntitiesData; ++ // Paper start - Handle oversized block entities in chunks ++ private final java.util.List> extraPackets = new java.util.ArrayList<>(); ++ private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750); ++ ++ public List> getExtraPackets() { ++ return this.extraPackets; ++ } ++ // Paper end - Handle oversized block entities in chunks + + // Paper start - Anti-Xray - Add chunk packet info + @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } +@@ -50,8 +58,18 @@ public class ClientboundLevelChunkPacketData { + extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); + // Paper end + this.blockEntitiesData = Lists.newArrayList(); ++ int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks + + for (Entry entry2 : chunk.getBlockEntities().entrySet()) { ++ // Paper start - Handle oversized block entities in chunks ++ if (++totalTileEntities > TE_LIMIT) { ++ var packet = entry2.getValue().getUpdatePacket(); ++ if (packet != null) { ++ this.extraPackets.add(packet); ++ continue; ++ } ++ } ++ // Paper end - Handle oversized block entities in chunks + this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entry2.getValue())); + } + } +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +index 8ead66c134688b11dca15f6509147e726f182e6a..cfcac0fdc130120cb1f8d97c6353d93db7ddf81b 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +@@ -83,4 +83,11 @@ public class ClientboundLevelChunkWithLightPacket implements Packet> getExtraPackets() { ++ return this.chunkData.getExtraPackets(); ++ } ++ // Paper end - Handle oversized block entities in chunks + } diff --git a/patches/server/1018-API-for-checking-sent-chunks.patch b/patches/server/1018-API-for-checking-sent-chunks.patch new file mode 100644 index 0000000000..54f091fb69 --- /dev/null +++ b/patches/server/1018-API-for-checking-sent-chunks.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Flo0 +Date: Mon, 8 Apr 2024 16:43:16 +0200 +Subject: [PATCH] API for checking sent chunks + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index 82e8ce73b77accd6a4210f88c9fccb325ae367d4..91c4219b2abf1b5be3dc35f7b8403884e1d36f8d 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -1070,5 +1070,10 @@ public final class RegionizedPlayerChunkLoader { + + // now all tickets should be removed, which is all of our external state + } ++ ++ // For external checks ++ public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() { ++ return this.sentChunks; ++ } + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 1bc343df0e7b8e6e3fadc970a4a4c8d787d93828..7eea190ce8a62960ecc42ff56a4ef71b754184fb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3483,6 +3483,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + // Paper end + ++ // Paper start - Add chunk view API ++ @Override ++ public Set getSentChunkKeys() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( ++ this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().clone() ++ ); ++ } ++ ++ @Override ++ public Set getSentChunks() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw(); ++ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); ++ final org.bukkit.World world = this.getWorld(); ++ ++ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().longIterator(); ++ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); ++ ++ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); ++ } ++ ++ @Override ++ public boolean isChunkSent(final long chunkKey) { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().contains(chunkKey); ++ } ++ // Paper end ++ + public Player.Spigot spigot() + { + return this.spigot; diff --git a/patches/server/1018-Configurable-Sand-Duping.patch b/patches/server/1018-Configurable-Sand-Duping.patch deleted file mode 100644 index cbc52697db..0000000000 --- a/patches/server/1018-Configurable-Sand-Duping.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Sat, 15 Jun 2024 22:01:39 -0400 -Subject: [PATCH] Configurable Sand Duping - - -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 caf9273cfafbbaaf8dc95498927383e608773665..b83be9bbb9f348da83c0fd1ecc7f65c8a58b45b9 100644 ---- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -@@ -419,7 +419,7 @@ public class FallingBlockEntity extends Entity { - boolean flag = (resourcekey1 == Level.END || resourcekey == Level.END) && resourcekey1 != resourcekey; - Entity entity = super.changeDimension(teleportTarget); - -- this.forceTickAfterTeleportToDuplicate = entity != null && flag; -+ this.forceTickAfterTeleportToDuplicate = entity != null && flag && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation; // Paper - return entity; - } - } diff --git a/patches/server/1019-Improve-boat-collision-performance.patch b/patches/server/1019-Improve-boat-collision-performance.patch new file mode 100644 index 0000000000..f646d2f8e9 --- /dev/null +++ b/patches/server/1019-Improve-boat-collision-performance.patch @@ -0,0 +1,69 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 2 Aug 2021 10:10:40 +0200 +Subject: [PATCH] Improve boat collision performance + + +diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java +index 42d7ecfab6f72517904451d9df3f0404b176fdb2..5869f2f5c72f736e6b077683327c64b9e0e0c733 100644 +--- a/src/main/java/net/minecraft/Util.java ++++ b/src/main/java/net/minecraft/Util.java +@@ -125,6 +125,7 @@ public class Util { + .filter(fileSystemProvider -> fileSystemProvider.getScheme().equalsIgnoreCase("jar")) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No jar file system provider found")); ++ public static final double COLLISION_EPSILON = 1.0E-7; // Paper - Improve boat collision performance + private static Consumer thePauser = message -> { + }; + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 25a7dfddb44a11f6e20c459141a61270c0c12d4c..affc39430e1f77e09768d2f614fcd5073d5a9270 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1436,7 +1436,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (!source.is(DamageTypeTags.IS_PROJECTILE)) { + Entity entity = source.getDirectEntity(); + +- if (entity instanceof LivingEntity) { ++ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance + LivingEntity entityliving = (LivingEntity) entity; + + this.blockUsingShield(entityliving); +@@ -1533,8 +1533,13 @@ public abstract class LivingEntity extends Entity implements Attackable { + double d0 = 0.0D; + double d1 = 0.0D; + Entity entity2 = source.getDirectEntity(); +- +- if (entity2 instanceof Projectile) { ++ // Paper start - improve boat collision performance ++ final boolean far = entity2.distanceToSqr(this) > (200.0D * 200.0D); ++ if (far) { ++ d0 = Math.random() - Math.random(); ++ d1 = Math.random() - Math.random(); ++ } else if (entity2 instanceof Projectile) { ++ // Paper end - improve boat collision performance + Projectile iprojectile = (Projectile) entity2; + DoubleDoubleImmutablePair doubledoubleimmutablepair = iprojectile.calculateHorizontalHurtKnockbackDirection(this, source); + +@@ -2325,7 +2330,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.hurtCurrentlyUsedShield((float) -event.getDamage(DamageModifier.BLOCKING)); + Entity entity = damagesource.getDirectEntity(); + +- if (!damagesource.is(DamageTypeTags.IS_PROJECTILE) && entity instanceof LivingEntity) { // Paper - Fix shield disable inconsistency ++ if (!damagesource.is(DamageTypeTags.IS_PROJECTILE) && entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Fix shield disable inconsistency & improve boat collision performance + this.blockUsingShield((LivingEntity) entity); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +index 907f751c859855484151fb5d607acee2f2a35076..edd30d872d151f953df28c8f6d40efa8d39f5e36 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +@@ -718,7 +718,7 @@ public class Boat extends VehicleEntity implements Leashable, VariantHolder -Date: Sun, 13 Aug 2023 15:41:52 -0700 -Subject: [PATCH] Improve performance of mass crafts - -When the server crafts all available items in CraftingMenu or InventoryMenu the game -checks either 4 or 9 times for each individual craft for a matching recipe for that container. -This check can be expensive if 64 total crafts are being performed with the recipe matching logic -being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching -recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. - -Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft -where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the -'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done -for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed -from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So -for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. - -After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the -initial recipe match. Then that recipe will be checked first for all future recipe match checks. - -diff --git a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -index 779d107a4d07820529273af5931421c09d1dc27f..4f6c8c43f5150e340704682accfbe2a5b1c5db19 100644 ---- a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -+++ b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -@@ -18,11 +18,11 @@ public interface CraftingContainer extends Container, StackedContentsCompatible - List getItems(); - - // CraftBukkit start -- default RecipeHolder getCurrentRecipe() { -+ default RecipeHolder getCurrentRecipe() { // Paper - use correct generic - return null; - } - -- default void setCurrentRecipe(RecipeHolder recipe) { -+ default void setCurrentRecipe(RecipeHolder recipe) { // Paper - use correct generic - } - // CraftBukkit end - -diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -index b572c905b2ef3cf79202c5f35eefd6a636e872f5..f9e4714cb0ee74505ed72200abed7756aa68dc90 100644 ---- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -@@ -79,6 +79,7 @@ public class CraftingMenu extends RecipeBookMenu - CraftingInput craftinginput = craftingInventory.asCraftInput(); - ServerPlayer entityplayer = (ServerPlayer) player; - ItemStack itemstack = ItemStack.EMPTY; -+ if (recipe == null) recipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first - Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftinginput, world, recipe); - craftingInventory.setCurrentRecipe(optional.orElse(null)); // CraftBukkit - -diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -index c75bc6bec06bb7c32eea81c0bc89441beb3d1150..37a89bf79017eb65f82276b054a70ddb5eb5e549 100644 ---- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java -+++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -@@ -63,7 +63,7 @@ public class ResultSlot extends Slot { - CraftingInput craftingInput = positioned.input(); - int i = positioned.left(); - int j = positioned.top(); -- NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, craftingInput, player.level()); -+ NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, craftingInput, player.level(), this.craftSlots.getCurrentRecipe()); // Paper - Perf: Improve mass crafting; check last recipe used first - - for (int k = 0; k < craftingInput.height(); k++) { - for (int l = 0; l < craftingInput.width(); l++) { -diff --git a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -index 341e1f70602ecdb4782c9cd74fa19135ac230d90..42207462c4e720677a5ddbba4129dae60420aaa3 100644 ---- a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -+++ b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -@@ -27,7 +27,7 @@ public class TransientCraftingContainer implements CraftingContainer { - - // CraftBukkit start - add fields - public List transaction = new java.util.ArrayList(); -- private RecipeHolder currentRecipe; -+ private RecipeHolder currentRecipe; // Paper - use correct generic - public Container resultInventory; - private Player owner; - private int maxStack = MAX_STACK; -@@ -72,12 +72,12 @@ public class TransientCraftingContainer implements CraftingContainer { - } - - @Override -- public RecipeHolder getCurrentRecipe() { -+ public RecipeHolder getCurrentRecipe() { // Paper - use correct generic - return this.currentRecipe; - } - - @Override -- public void setCurrentRecipe(RecipeHolder currentRecipe) { -+ public void setCurrentRecipe(RecipeHolder currentRecipe) { // Paper - use correct generic - this.currentRecipe = currentRecipe; - } - -diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -index 407f3c1938b5b5d893b09705fe4930dbdafa3c8e..de7c77c1b25680ecc65f0f43f2391aff269a974f 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -+++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -@@ -111,13 +111,20 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { - } - - public > Optional> getRecipeFor(RecipeType type, I input, Level world, @Nullable RecipeHolder recipe) { -- // CraftBukkit start -- List> list = this.byType(type).stream().filter((recipeholder1) -> { -- return recipeholder1.value().matches(input, world); -- }).toList(); -- Optional> recipe1 = (list.isEmpty() || input.isEmpty()) ? Optional.empty() : (recipe != null && recipe.value().matches(input, world) ? Optional.of(recipe) : Optional.of(list.getLast())); // CraftBukkit - SPIGOT-4638: last recipe gets priority -- return recipe1; -- // CraftBukkit end -+ // Paper start - fix upstream's complete screw up of checking last used recipe -+ if (input.isEmpty()) { -+ return Optional.empty(); -+ } else if (recipe != null && recipe.value().matches(input, world)) { -+ return Optional.of(recipe); -+ } else { -+ // CraftBukkit start -+ List> list = this.byType(type).stream().filter((recipeholder1) -> { -+ return recipeholder1.value().matches(input, world); -+ }).toList(); -+ return list.isEmpty() ? Optional.empty() : Optional.of(list.getLast()); // CraftBukkit - SPIGOT-4638: last recipe gets priority -+ // CraftBukkit end -+ } -+ // Paper end - } - - public > List> getAllRecipesFor(RecipeType type) { -@@ -137,7 +144,12 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { - } - - public > NonNullList getRemainingItemsFor(RecipeType type, I input, Level world) { -- Optional> optional = this.getRecipeFor(type, input, world); -+ // Paper start - Perf: improve performance of mass crafts -+ return this.getRemainingItemsFor(type, input, world, null); -+ } -+ public > NonNullList getRemainingItemsFor(RecipeType type, I input, Level world, @Nullable RecipeHolder previousRecipe) { -+ Optional> optional = this.getRecipeFor(type, input, world, previousRecipe); -+ // Paper end - Perf: improve performance of mass crafts - - if (optional.isPresent()) { - return ((RecipeHolder) optional.get()).value().getRemainingItems(input); diff --git a/patches/server/1020-Configurable-Sand-Duping.patch b/patches/server/1020-Configurable-Sand-Duping.patch new file mode 100644 index 0000000000..cbc52697db --- /dev/null +++ b/patches/server/1020-Configurable-Sand-Duping.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sat, 15 Jun 2024 22:01:39 -0400 +Subject: [PATCH] Configurable Sand Duping + + +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 caf9273cfafbbaaf8dc95498927383e608773665..b83be9bbb9f348da83c0fd1ecc7f65c8a58b45b9 100644 +--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -419,7 +419,7 @@ public class FallingBlockEntity extends Entity { + boolean flag = (resourcekey1 == Level.END || resourcekey == Level.END) && resourcekey1 != resourcekey; + Entity entity = super.changeDimension(teleportTarget); + +- this.forceTickAfterTeleportToDuplicate = entity != null && flag; ++ this.forceTickAfterTeleportToDuplicate = entity != null && flag && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation; // Paper + return entity; + } + } diff --git a/patches/server/1020-Properly-resend-entities.patch b/patches/server/1020-Properly-resend-entities.patch deleted file mode 100644 index de5a776d10..0000000000 --- a/patches/server/1020-Properly-resend-entities.patch +++ /dev/null @@ -1,232 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Wed, 7 Dec 2022 17:25:19 -0500 -Subject: [PATCH] Properly resend entities - -This resolves some issues which caused entities to not be resent correctly. -Entities that are interacted with need to be resent to the client, so we resend all the entity -data to the player whilst making sure not to clear dirty entries from the tracker. This makes -sure that values will be correctly updated to other players. - -This also adds utilities to aid in further preventing entity desyncs. - -This also also fixes the bug causing cancelling PlayerInteractEvent to cause items to continue -to be used despite being cancelled on the server. - -For example, items being consumed but never finishing, shields being put up, etc. -The underlying issue of this is that the client modifies their synced data values, -and so we have to (forcibly) resend them in order for the client to reset their using item state. - -See: https://github.com/PaperMC/Paper/pull/1896 - -== AT == -public net.minecraft.server.level.ChunkMap$TrackedEntity serverEntity - -diff --git a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -index 02bf2705ca1c99023a83a22d92e1962181102297..0f99733660f91280e4c6262cf75b3c9cae86f65a 100644 ---- a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -+++ b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -@@ -50,7 +50,7 @@ public class SynchedEntityData { - } - } - -- private SynchedEntityData.DataItem getItem(EntityDataAccessor key) { -+ public SynchedEntityData.DataItem getItem(EntityDataAccessor key) { // Paper - public - return (SynchedEntityData.DataItem) this.itemsById[key.id()]; // CraftBukkit - decompile error - } - -@@ -151,6 +151,20 @@ public class SynchedEntityData { - } - } - -+ // Paper start -+ // We need to pack all as we cannot rely on "non default values" or "dirty" ones. -+ // Because these values can possibly be desynced on the client. -+ @Nullable -+ public List> packAll() { -+ final List> list = new ArrayList<>(); -+ for (final DataItem dataItem : this.itemsById) { -+ list.add(dataItem.value()); -+ } -+ -+ return list; -+ } -+ // Paper end -+ - public static class DataItem { - - final EntityDataAccessor accessor; -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index e9dcdb1e09e84a9b451034ff4bdfa6eae2dd1c04..24b1715397ba8e6f5e9841a030d0e3d964356f89 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -561,6 +561,7 @@ public class ServerPlayerGameMode { - } - // Paper end - extend Player Interact cancellation - player.getBukkitEntity().updateInventory(); // SPIGOT-2867 -+ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items - return (event.useItemInHand() != Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS; - } else if (this.gameModeForPlayer == GameType.SPECTATOR) { - MenuProvider itileinventory = iblockdata.getMenuProvider(world, blockposition); -@@ -612,6 +613,11 @@ public class ServerPlayerGameMode { - - return enuminteractionresult; - } else { -+ // Paper start - Properly cancel usable items; Cancel only if cancelled + if the interact result is different from default response -+ if (this.interactResult && this.interactResult != cancelledItem) { -+ this.player.resyncUsingItem(this.player); -+ } -+ // Paper end - Properly cancel usable items - return InteractionResult.PASS; - } - } -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index a8debfad8c8e66099f8a9aedc6f1971a8576dade..7796e191747be545e744564a2b0b65790f69114d 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -1949,6 +1949,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - if (cancelled) { -+ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items - this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524 - return; - } -@@ -2718,7 +2719,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a - if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { -- entity.getBukkitEntity().update(ServerGamePacketListenerImpl.this.player); -+ entity.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it - ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); - } - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0d0b07c9199be9ca0d5ac3feb1d44f149ba69283..3d30427e75bdfb9cf453fb5cd2a344227da1641a 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -393,7 +393,7 @@ public abstract class PlayerList { - ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now - // CraftBukkit end - -- player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn -+ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE - - this.sendLevelInfo(player, worldserver1); - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 5d7bc6470ab3818b0a189aab18ff26c0180e3912..2519c08a45ce3febcf51ac308ad98ac0e2657cf3 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -664,13 +664,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - // CraftBukkit start - public void refreshEntityData(ServerPlayer to) { -- List> list = this.getEntityData().getNonDefaultValues(); -+ List> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default - -- if (list != null) { -+ if (list != null && to.getBukkitEntity().canSee(this.getBukkitEntity())) { // Paper - to.connection.send(new ClientboundSetEntityDataPacket(this.getId(), list)); - } - } - // CraftBukkit end -+ // Paper start -+ // This method should only be used if the data of an entity could have become desynced -+ // due to interactions on the client. -+ public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { -+ if (this.tracker == null) { -+ return; -+ } -+ -+ if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { -+ final net.minecraft.server.level.ServerEntity serverEntity = this.tracker.serverEntity; -+ final List> list = new java.util.ArrayList<>(); -+ serverEntity.sendPairingData(player, list::add); -+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(list)); -+ } -+ } -+ -+ // This method allows you to specifically resend certain data accessor keys to the client -+ public void resendPossiblyDesyncedDataValues(List> keys, ServerPlayer to) { -+ if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) { -+ return; -+ } -+ -+ final List> values = new java.util.ArrayList<>(keys.size()); -+ for (final EntityDataAccessor key : keys) { -+ final SynchedEntityData.DataItem synchedValue = this.entityData.getItem(key); -+ values.add(synchedValue.value()); -+ } -+ -+ to.connection.send(new ClientboundSetEntityDataPacket(this.id, values)); -+ } -+ // Paper end - - public boolean equals(Object object) { - return object instanceof Entity ? ((Entity) object).id == this.id : false; -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 25a7dfddb44a11f6e20c459141a61270c0c12d4c..e980c8c356b30d25e2fc5a73b91ad2c6edd4fe05 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3840,6 +3840,11 @@ public abstract class LivingEntity extends Entity implements Attackable { - return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; - } - -+ // Paper start - Properly cancel usable items -+ public void resyncUsingItem(ServerPlayer serverPlayer) { -+ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); -+ } -+ // Paper end - Properly cancel usable items - private void updatingUsingItem() { - if (this.isUsingItem()) { - if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -index b586116d8cca1585f9c9e618ed4d0cb2ef2747be..acf38ef6d8de8b15cf2b09eb7bda390c4e446e9a 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -@@ -108,8 +108,7 @@ public interface Bucketable { - itemstack1 = CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket()); - if (playerBucketFishEvent.isCancelled()) { - ((ServerPlayer) player).containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket -- entity.getBukkitEntity().update((ServerPlayer) player); // We need to play out these packets as the client assumes the fish is gone -- entity.refreshEntityData((ServerPlayer) player); // Need to send data such as the display name to client -+ entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper - return Optional.of(InteractionResult.FAIL); - } - entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F); -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index b8eb9166e44da8745a056bf68f2f9316ce25d7a7..2cde808bfa797256409879505ba205a71f381981 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -1012,7 +1012,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - return; - } - -- entityTracker.broadcast(this.getHandle().getAddEntityPacket(entityTracker.serverEntity)); -+ // Paper start - resend possibly desynced entity instead of add entity packet -+ for (final ServerPlayerConnection connection : entityTracker.seenBy) { -+ this.getHandle().resendPossiblyDesyncedEntityData(connection.getPlayer()); -+ } -+ // Paper end - resend possibly desynced entity instead of add entity packet - } - - public void update(ServerPlayer player) { -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -index f3a9b3380246fb2dd4b60a8d1a94c5dfed98d316..350ad61ab3fe66abd528e353b431a4a6dac17506 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -@@ -39,9 +39,11 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame { - protected void update() { - super.update(); - -+ // Paper start, don't mark as dirty as this is handled in super.update() - // mark dirty, so that the client gets updated with item and rotation -- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); -- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); -+ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); -+ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); -+ // Paper end - - // update redstone - if (!this.getHandle().generation) { diff --git a/patches/server/1021-Optimise-general-POI-access.patch b/patches/server/1021-Optimise-general-POI-access.patch new file mode 100644 index 0000000000..f8e4fb9c3d --- /dev/null +++ b/patches/server/1021-Optimise-general-POI-access.patch @@ -0,0 +1,1059 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 31 Jan 2021 02:29:24 -0800 +Subject: [PATCH] Optimise general POI access + +There are a couple of problems with mojang's POI code. +Firstly, it's all streams. Unsurprisingly, stacking +streams on top of each other is horrible for performance +and ultimately took up half of a villager's tick! + +Secondly, sometime's the search radius is large and there are +a significant number of poi entries per chunk section. Even +removing streams at this point doesn't help much. The only solution +is to start at the search point and iterate outwards. This +type of approach shows massive gains for portals, simply because +we can avoid sync loading a large area of chunks. I also tested +a massive farm I found in JellySquid's discord, which showed +to benefit significantly simply because the farm had so many +portal blocks that searching through them all was very slow. + +Great care has been taken so that behavior remains identical to +vanilla, however I cannot account for oddball Stream API +implementations, if they even exist (streams can technically +be loose with iteration order in a sorted stream given its +source stream is not tagged with ordered, and mojang does not +tag the source stream as ordered). However in my testing on openjdk +there showed no difference, as expected. + +This patch also specifically optimises other areas of code to +use PoiAccess. For example, some villager AI and portaling code +had to be specifically modified. + +diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d8190b56d216b0e48bfc2d5739bdf533bf95b425 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/PoiAccess.java +@@ -0,0 +1,804 @@ ++package io.papermc.paper.util; ++ ++import com.mojang.datafixers.util.Pair; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import java.util.function.BiPredicate; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Holder; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.ai.village.poi.PoiManager; ++import net.minecraft.world.entity.ai.village.poi.PoiRecord; ++import net.minecraft.world.entity.ai.village.poi.PoiSection; ++import net.minecraft.world.entity.ai.village.poi.PoiType; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.Set; ++import java.util.function.Predicate; ++ ++/** ++ * Provides optimised access to POI data. All returned values will be identical to vanilla. ++ */ ++public final class PoiAccess { ++ ++ protected static double clamp(final double val, final double min, final double max) { ++ return (val < min ? min : (val > max ? max : val)); ++ } ++ ++ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, ++ final double boxMaxX, final double boxMaxY, final double boxMaxZ, ++ ++ final double circleX, final double circleY, final double circleZ) { ++ // is the circle center inside the box? ++ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { ++ return 0.0; ++ } ++ ++ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; ++ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; ++ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; ++ ++ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; ++ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; ++ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; ++ ++ double centerDiffX = circleX - boxCenterX; ++ double centerDiffY = circleY - boxCenterY; ++ double centerDiffZ = circleZ - boxCenterZ; ++ ++ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); ++ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); ++ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); ++ ++ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); ++ } ++ ++ ++ // key is: ++ // upper 32 bits: ++ // upper 16 bits: max y section ++ // lower 16 bits: min y section ++ // lower 32 bits: ++ // upper 16 bits: section ++ // lower 16 bits: radius ++ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { ++ return ( ++ (maxSection & 0xFFFFL) << (64 - 16) ++ | (minSection & 0xFFFFL) << (64 - 32) ++ | (section & 0xFFFFL) << (64 - 48) ++ | (radius & 0xFFFFL) << (64 - 64) ++ ); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final Set ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(record.getPos()); ++ } ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; ++ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); ++ } ++ ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final List closestRecords = new ArrayList<>(); ++ double closestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() ++ ); ++ if (sectionDistanceSquared > closestDistanceSquared) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > closestDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange < closestDistanceSquared) { ++ closestRecords.clear(); ++ closestDistanceSquared = dataRange; ++ } ++ closestRecords.add(poiData); ++ } ++ } ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecords.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecords); ++ } ++ ++ // finds the closest poi entry pos. ++ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findNearestPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // finds the closest `max` poi entry positions. ++ public static void findNearestPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ // finds the closest poi entry. ++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ++ 1, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // finds the closest `max` poi entries. ++ public static void findNearestPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); ++ int totalRecords = 0; ++ double furthestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() ++ ); ++ ++ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > maxDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared && totalRecords >= max) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared) { ++ // we know totalRecords < max, so this entry is now our furthest ++ furthestDistanceSquared = dataRange; ++ } ++ ++ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { ++ return new ArrayList<>(); ++ }).add(poiData); ++ ++ if (++totalRecords >= max) { ++ if (closestRecords.size() >= 2) { ++ int entriesInClosest = 0; ++ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); ++ double nextFurthestDistanceSquared = 0.0; ++ ++ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ entriesInClosest += recordEntry.getValue().size(); ++ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); ++ } ++ ++ if (entriesInClosest >= max) { ++ // the last set of entries at range wont even be considered for sure... nuke em ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ totalRecords -= recordEntry.getValue().size(); ++ iterator.remove(); ++ ++ furthestDistanceSquared = nextFurthestDistanceSquared; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ final List closestRecordsUnsorted = new ArrayList<>(); ++ ++ // we're done here, so now just flatten the map and sort it. ++ ++ for (final List records : closestRecords.values()) { ++ closestRecordsUnsorted.addAll(records); ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecordsUnsorted.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller section y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // trim out any entries exceeding our maximum ++ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { ++ closestRecordsUnsorted.remove(i); ++ } ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecordsUnsorted); ++ } ++ ++ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findAnyPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ public static void findAnyPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findAnyPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ public static void findAnyPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List ret) { ++ // the biggest issue with the original mojang implementation is that they chain so many streams together ++ // the amount of streams chained just rolls performance, even if nothing is iterated over ++ final Predicate occupancyFilter = occupancy.getTest(); ++ final double rangeSquared = range * range; ++ ++ int added = 0; ++ ++ // First up, we need to iterate the chunks ++ // all the values here are in chunk sections ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() - range) >> 4); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() + range) >> 4); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ // Vanilla iterates by x until max is reached then increases z ++ // vanilla also searches by increasing Y section value ++ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { ++ for (int currX = lowerX; currX <= upperX; ++currX) { ++ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : ++ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); ++ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); ++ if (poiSection == null) { ++ continue; ++ } ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ // found one! ++ ret.add(poiData); ++ if (++added >= max) { ++ return; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ private PoiAccess() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -71,11 +71,11 @@ public class AcquirePoi { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( +- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE +- ) +- .limit(5L) +- .collect(Collectors.toSet()); ++ // Paper start - optimise POI access ++ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); ++ Set, BlockPos>> set = new java.util.HashSet<>(poiposes); ++ // Paper end - optimise POI access + Path path = findPathToPois(entity, set); + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllWithType( +- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY +- ) +- .collect(Collectors.toSet()); +- Path path = AcquirePoi.findPathToPois(entity, set); ++ // Paper start - optimise POI access ++ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ // don't ask me why it's unbounded. ask mojang. ++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); ++ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); ++ // Paper end - optimise POI access + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); + Optional> optional = poiManager.getType(blockPos); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 274ddf479d38495d84838f9cd73c13d2841c3b44..0be7346f9c5e89ea03c69f7c8ba4733d2c7759fc 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -268,36 +268,45 @@ public class PoiManager extends SectionStorage implements ca.spotted + public Optional find( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end + } + + public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end - re-route to faster logic + } + + public Optional, BlockPos>> findClosestWithType( + Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) +- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); ++ // Paper start - re-route to faster logic ++ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( ++ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false ++ )); ++ // Paper end - re-route to faster logic + } + + public Optional findClosest( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .filter(posPredicate) +- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end - re-route to faster logic + } + + public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> biPredicate, BlockPos pos, int radius) { +- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) +- .filter(poi -> biPredicate.test(poi.getPoiType(), poi.getPos())) +- .findFirst() ++ // Paper start - re-route to faster logic ++ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( ++ this, typePredicate, biPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false ++ ); ++ return Optional.ofNullable(closest) ++ // Paper end - re-route to faster logic + .map(poi -> { + poi.acquireTicket(); + return poi.getPos(); +@@ -312,8 +321,21 @@ public class PoiManager extends SectionStorage implements ca.spotted + int radius, + RandomSource random + ) { +- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); +- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); ++ // Paper start - re-route to faster logic ++ List list = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( ++ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list ++ ); ++ ++ // the old method shuffled the list and then tried to find the first element in it that ++ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a ++ // shuffle entirely, and just pick a random element from list ++ if (list.isEmpty()) { ++ return Optional.empty(); ++ } ++ ++ return Optional.of(list.get(random.nextInt(list.size())).getPos()); ++ // Paper end - re-route to faster logic + } + + public boolean release(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index a6c0e89cb645693034f8e90ac2de8f2da457453c..11e895d837794d79a76303b912092096bd7d07a8 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -26,7 +26,7 @@ import org.slf4j.Logger; + public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system + private static final Logger LOGGER = LogUtils.getLogger(); + private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); +- private final Map, Set> byType = Maps.newHashMap(); ++ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor + private final Runnable setDirty; + private boolean isValid; + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +index c7ed3eb80f6e8b918434153093644776866aa220..21de1b95f2a5d136149447472e871f675760ba1a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +@@ -81,11 +81,11 @@ public abstract class SectionStorage implements AutoCloseable, ca.spottedleaf + } + + @Nullable +- protected Optional get(long pos) { ++ public Optional get(long pos) { // Paper - public + return this.storage.get(pos); + } + +- protected Optional getOrLoad(long pos) { ++ public Optional getOrLoad(long pos) { // Paper - public + if (this.outsideStoredRange(pos)) { + return Optional.empty(); + } else { +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 fd04a50183ccb1f21fc6efa70256e1bb4db2d6d4..49f7ba292b82bac1643cc07aa576f3c37b8e8ab3 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -53,17 +53,39 @@ public class PortalForcer { + // int i = flag ? 16 : 128; + // CraftBukkit end + +- villageplace.ensureLoadedAndValid(this.level, blockposition, i); +- Stream stream = villageplace.getInSquare((holder) -> { // CraftBukkit - decompile error +- return holder.is(PoiTypes.NETHER_PORTAL); +- }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos); +- +- Objects.requireNonNull(worldborder); +- 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); +- }).thenComparingInt(Vec3i::getY)); ++ // Paper start - optimise portals ++ Optional optional; ++ java.util.List records = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( ++ villageplace, ++ type -> type.is(PoiTypes.NETHER_PORTAL), ++ (BlockPos pos) -> { ++ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); ++ if (!lowest.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) ++ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { ++ // why would we generate the chunk? ++ return false; ++ } ++ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage ++ return false; ++ } ++ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); ++ }, ++ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records ++ ); ++ ++ // this gets us most of the way there, but we bias towards lower y values. ++ BlockPos lowestPos = null; ++ for (PoiRecord record : records) { ++ if (lowestPos == null) { ++ lowestPos = record.getPos(); ++ } else if (lowestPos.getY() > record.getPos().getY()) { ++ lowestPos = record.getPos(); ++ } ++ } ++ // now we're done ++ return Optional.ofNullable(lowestPos); ++ // Paper end - optimise portals + } + + public Optional createPortal(BlockPos pos, Direction.Axis axis) { diff --git a/patches/server/1021-Registry-Modification-API.patch b/patches/server/1021-Registry-Modification-API.patch deleted file mode 100644 index c45b119038..0000000000 --- a/patches/server/1021-Registry-Modification-API.patch +++ /dev/null @@ -1,1421 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Mon, 27 Feb 2023 18:28:39 -0800 -Subject: [PATCH] Registry Modification API - -== AT == -public net.minecraft.core.MappedRegistry validateWrite(Lnet/minecraft/resources/ResourceKey;)V -public net.minecraft.resources.RegistryOps lookupProvider -public net.minecraft.resources.RegistryOps$HolderLookupAdapter - -diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java -index 1e098dc25bd338ff179491ff3382ac56aad9948e..a688af29273ebfbb4f75dd74cd30627fc481c96c 100644 ---- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java -+++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java -@@ -2,6 +2,7 @@ package io.papermc.paper.registry; - - import io.papermc.paper.adventure.PaperAdventure; - import io.papermc.paper.registry.entry.RegistryEntry; -+import io.papermc.paper.registry.tag.TagKey; - import java.util.Collections; - import java.util.IdentityHashMap; - import java.util.List; -@@ -46,6 +47,7 @@ import org.checkerframework.framework.qual.DefaultQualifier; - - import static io.papermc.paper.registry.entry.RegistryEntry.apiOnly; - import static io.papermc.paper.registry.entry.RegistryEntry.entry; -+import static io.papermc.paper.registry.entry.RegistryEntry.writable; - - @DefaultQualifier(NonNull.class) - public final class PaperRegistries { -@@ -128,6 +130,15 @@ public final class PaperRegistries { - return ResourceKey.create((ResourceKey>) PaperRegistries.registryToNms(typedKey.registryKey()), PaperAdventure.asVanilla(typedKey.key())); - } - -+ public static TagKey fromNms(final net.minecraft.tags.TagKey tagKey) { -+ return TagKey.create(registryFromNms(tagKey.registry()), CraftNamespacedKey.fromMinecraft(tagKey.location())); -+ } -+ -+ @SuppressWarnings({"unchecked", "RedundantCast"}) -+ public static net.minecraft.tags.TagKey toNms(final TagKey tagKey) { -+ return net.minecraft.tags.TagKey.create((ResourceKey>) registryToNms(tagKey.registryKey()), PaperAdventure.asVanilla(tagKey.key())); -+ } -+ - private PaperRegistries() { - } - } -diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java -index d591e3a2e19d5358a0d25a5a681368943622d231..f05ebf453406a924da3de6fb250f4793a1b3c612 100644 ---- a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java -+++ b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java -@@ -80,6 +80,14 @@ public class PaperRegistryAccess implements RegistryAccess { - return possiblyUnwrap(registryHolder.get()); - } - -+ public > WritableCraftRegistry getWritableRegistry(final RegistryKey key) { -+ final Registry registry = this.getRegistry(key); -+ if (registry instanceof WritableCraftRegistry) { -+ return (WritableCraftRegistry) registry; -+ } -+ throw new IllegalArgumentException(key + " does not point to a writable registry"); -+ } -+ - private static Registry possiblyUnwrap(final Registry registry) { - if (registry instanceof final DelayedRegistry delayedRegistry) { // if not coming from legacy, unwrap the delayed registry - return delayedRegistry.delegate(); -diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..528c6ee1739d92f766f3904acd7fc5734c93388a ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java -@@ -0,0 +1,26 @@ -+package io.papermc.paper.registry; -+ -+import io.papermc.paper.registry.data.util.Conversions; -+import net.minecraft.resources.RegistryOps; -+import org.checkerframework.checker.nullness.qual.Nullable; -+ -+public interface PaperRegistryBuilder extends RegistryBuilder { -+ -+ M build(); -+ -+ @FunctionalInterface -+ interface Filler> { -+ -+ B fill(Conversions conversions, TypedKey key, @Nullable M nms); -+ -+ default Factory asFactory() { -+ return (lookup, key) -> this.fill(lookup, key, null); -+ } -+ } -+ -+ @FunctionalInterface -+ interface Factory> { -+ -+ B create(Conversions conversions, TypedKey key); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a7f2b264d4f37f5293ae72195b4c78faf35351c9 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java -@@ -0,0 +1,183 @@ -+package io.papermc.paper.registry; -+ -+import com.google.common.base.Preconditions; -+import com.mojang.serialization.Lifecycle; -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.entrypoint.Entrypoint; -+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; -+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.entry.RegistryEntry; -+import io.papermc.paper.registry.entry.RegistryEntryInfo; -+import io.papermc.paper.registry.event.RegistryEntryAddEventImpl; -+import io.papermc.paper.registry.event.RegistryEventMap; -+import io.papermc.paper.registry.event.RegistryEventProvider; -+import io.papermc.paper.registry.event.RegistryFreezeEvent; -+import io.papermc.paper.registry.event.RegistryFreezeEventImpl; -+import io.papermc.paper.registry.event.type.RegistryEntryAddEventType; -+import io.papermc.paper.registry.event.type.RegistryEntryAddEventTypeImpl; -+import io.papermc.paper.registry.event.type.RegistryLifecycleEventType; -+import java.util.Optional; -+import net.kyori.adventure.key.Key; -+import net.minecraft.core.Holder; -+import net.minecraft.core.MappedRegistry; -+import net.minecraft.core.RegistrationInfo; -+import net.minecraft.core.Registry; -+import net.minecraft.core.WritableRegistry; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.resources.ResourceLocation; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.intellij.lang.annotations.Subst; -+ -+public final class PaperRegistryListenerManager { -+ -+ public static final PaperRegistryListenerManager INSTANCE = new PaperRegistryListenerManager(); -+ -+ public final RegistryEventMap valueAddHooks = new RegistryEventMap("value add"); -+ public final RegistryEventMap freezeHooks = new RegistryEventMap("freeze"); -+ -+ private PaperRegistryListenerManager() { -+ } -+ -+ /** -+ * For {@link Registry#register(Registry, String, Object)} -+ */ -+ public M registerWithListeners(final Registry registry, final String id, final M nms) { -+ return this.registerWithListeners(registry, ResourceLocation.withDefaultNamespace(id), nms); -+ } -+ -+ /** -+ * For {@link Registry#register(Registry, ResourceLocation, Object)} -+ */ -+ public M registerWithListeners(final Registry registry, final ResourceLocation loc, final M nms) { -+ return this.registerWithListeners(registry, ResourceKey.create(registry.key(), loc), nms); -+ } -+ -+ /** -+ * For {@link Registry#register(Registry, ResourceKey, Object)} -+ */ -+ public M registerWithListeners(final Registry registry, final ResourceKey key, final M nms) { -+ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, PaperRegistryListenerManager::registerWithInstance, BuiltInRegistries.BUILT_IN_CONVERSIONS); -+ } -+ -+ /** -+ * For {@link Registry#registerForHolder(Registry, ResourceLocation, Object)} -+ */ -+ public Holder.Reference registerForHolderWithListeners(final Registry registry, final ResourceLocation loc, final M nms) { -+ return this.registerForHolderWithListeners(registry, ResourceKey.create(registry.key(), loc), nms); -+ } -+ -+ /** -+ * For {@link Registry#registerForHolder(Registry, ResourceKey, Object)} -+ */ -+ public Holder.Reference registerForHolderWithListeners(final Registry registry, final ResourceKey key, final M nms) { -+ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, WritableRegistry::register, BuiltInRegistries.BUILT_IN_CONVERSIONS); -+ } -+ -+ public void registerWithListeners( -+ final Registry registry, -+ final ResourceKey key, -+ final M nms, -+ final RegistrationInfo registrationInfo, -+ final Conversions conversions -+ ) { -+ this.registerWithListeners(registry, key, nms, registrationInfo, WritableRegistry::register, conversions); -+ } -+ -+ // TODO remove Keyed -+ public , R> R registerWithListeners( -+ final Registry registry, -+ final ResourceKey key, -+ final M nms, -+ final RegistrationInfo registrationInfo, -+ final RegisterMethod registerMethod, -+ final Conversions conversions -+ ) { -+ Preconditions.checkState(LaunchEntryPointHandler.INSTANCE.hasEntered(Entrypoint.BOOTSTRAPPER), registry.key() + " tried to run modification listeners before bootstrappers have been called"); // verify that bootstrappers have been called -+ final @Nullable RegistryEntryInfo entry = PaperRegistries.getEntry(registry.key()); -+ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddHooks.hasHooks(entry.apiKey())) { -+ return registerMethod.register((WritableRegistry) registry, key, nms, registrationInfo); -+ } -+ final RegistryEntry.Modifiable modifiableEntry = RegistryEntry.Modifiable.asModifiable(entry); -+ @SuppressWarnings("PatternValidation") final TypedKey typedKey = TypedKey.create(entry.apiKey(), Key.key(key.location().getNamespace(), key.location().getPath())); -+ final B builder = modifiableEntry.fillBuilder(conversions, typedKey, nms); -+ return this.registerWithListeners(registry, modifiableEntry, key, nms, builder, registrationInfo, registerMethod, conversions); -+ } -+ -+ > void registerWithListeners( // TODO remove Keyed -+ final WritableRegistry registry, -+ final RegistryEntryInfo entry, -+ final ResourceKey key, -+ final B builder, -+ final RegistrationInfo registrationInfo, -+ final Conversions conversions -+ ) { -+ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddHooks.hasHooks(entry.apiKey())) { -+ registry.register(key, builder.build(), registrationInfo); -+ return; -+ } -+ this.registerWithListeners(registry, RegistryEntry.Modifiable.asModifiable(entry), key, null, builder, registrationInfo, WritableRegistry::register, conversions); -+ } -+ -+ public , R> R registerWithListeners( // TODO remove Keyed -+ final Registry registry, -+ final RegistryEntry.Modifiable entry, -+ final ResourceKey key, -+ final @Nullable M oldNms, -+ final B builder, -+ RegistrationInfo registrationInfo, -+ final RegisterMethod registerMethod, -+ final Conversions conversions -+ ) { -+ @Subst("namespace:key") final ResourceLocation beingAdded = key.location(); -+ @SuppressWarnings("PatternValidation") final TypedKey typedKey = TypedKey.create(entry.apiKey(), Key.key(beingAdded.getNamespace(), beingAdded.getPath())); -+ final RegistryEntryAddEventImpl event = entry.createEntryAddEvent(typedKey, builder, conversions); -+ LifecycleEventRunner.INSTANCE.callEvent(this.valueAddHooks.getHook(entry.apiKey()), event); -+ if (oldNms != null) { -+ ((MappedRegistry) registry).clearIntrusiveHolder(oldNms); -+ } -+ final M newNms = event.builder().build(); -+ if (oldNms != null && !newNms.equals(oldNms)) { -+ registrationInfo = new RegistrationInfo(Optional.empty(), Lifecycle.experimental()); -+ } -+ return registerMethod.register((WritableRegistry) registry, key, newNms, registrationInfo); -+ } -+ -+ private static M registerWithInstance(final WritableRegistry writableRegistry, final ResourceKey key, final M value, final RegistrationInfo registrationInfo) { -+ writableRegistry.register(key, value, registrationInfo); -+ return value; -+ } -+ -+ @FunctionalInterface -+ public interface RegisterMethod { -+ -+ R register(WritableRegistry writableRegistry, ResourceKey key, M value, RegistrationInfo registrationInfo); -+ } -+ -+ public > void runFreezeListeners(final ResourceKey> resourceKey, final Conversions conversions) { -+ final @Nullable RegistryEntryInfo entry = PaperRegistries.getEntry(resourceKey); -+ if (!RegistryEntry.Addable.isAddable(entry) || !this.freezeHooks.hasHooks(entry.apiKey())) { -+ return; -+ } -+ final RegistryEntry.Addable writableEntry = RegistryEntry.Addable.asAddable(entry); -+ final WritableCraftRegistry writableRegistry = PaperRegistryAccess.instance().getWritableRegistry(entry.apiKey()); -+ final RegistryFreezeEventImpl event = writableEntry.createFreezeEvent(writableRegistry, conversions); -+ LifecycleEventRunner.INSTANCE.callEvent(this.freezeHooks.getHook(entry.apiKey()), event); -+ } -+ -+ public > RegistryEntryAddEventType getRegistryValueAddEventType(final RegistryEventProvider type) { -+ if (!RegistryEntry.Modifiable.isModifiable(PaperRegistries.getEntry(type.registryKey()))) { -+ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryEntryAddEvent"); -+ } -+ return this.valueAddHooks.getOrCreate(type, RegistryEntryAddEventTypeImpl::new); -+ } -+ -+ public > LifecycleEventType.Prioritizable> getRegistryFreezeEventType(final RegistryEventProvider type) { -+ if (!RegistryEntry.Addable.isAddable(PaperRegistries.getEntry(type.registryKey()))) { -+ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryFreezeEvent"); -+ } -+ return this.freezeHooks.getOrCreate(type, RegistryLifecycleEventType::new); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..78317c7ab42a666f19634593a8f3b696700764c8 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java -@@ -0,0 +1,92 @@ -+package io.papermc.paper.registry; -+ -+import com.mojang.serialization.Lifecycle; -+import io.papermc.paper.adventure.PaperAdventure; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.entry.RegistryEntry; -+import io.papermc.paper.registry.event.WritableRegistry; -+import java.util.Optional; -+import java.util.function.BiFunction; -+import java.util.function.Consumer; -+import net.minecraft.core.MappedRegistry; -+import net.minecraft.core.RegistrationInfo; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.Keyed; -+import org.bukkit.NamespacedKey; -+import org.bukkit.craftbukkit.CraftRegistry; -+import org.bukkit.craftbukkit.util.ApiVersion; -+import org.checkerframework.checker.nullness.qual.Nullable; -+ -+public class WritableCraftRegistry> extends CraftRegistry { -+ -+ private static final RegistrationInfo FROM_PLUGIN = new RegistrationInfo(Optional.empty(), Lifecycle.experimental()); -+ -+ private final RegistryEntry.BuilderHolder entry; -+ private final MappedRegistry registry; -+ private final PaperRegistryBuilder.Factory builderFactory; -+ private final BiFunction minecraftToBukkit; -+ -+ public WritableCraftRegistry( -+ final RegistryEntry.BuilderHolder entry, -+ final Class classToPreload, -+ final MappedRegistry registry, -+ final BiFunction serializationUpdater, -+ final PaperRegistryBuilder.Factory builderFactory, -+ final BiFunction minecraftToBukkit -+ ) { -+ super(classToPreload, registry, null, serializationUpdater); -+ this.entry = entry; -+ this.registry = registry; -+ this.builderFactory = builderFactory; -+ this.minecraftToBukkit = minecraftToBukkit; -+ } -+ -+ public void register(final TypedKey key, final Consumer value, final Conversions conversions) { -+ final ResourceKey resourceKey = ResourceKey.create(this.registry.key(), PaperAdventure.asVanilla(key.key())); -+ this.registry.validateWrite(resourceKey); -+ final B builder = this.newBuilder(conversions, key); -+ value.accept(builder); -+ PaperRegistryListenerManager.INSTANCE.registerWithListeners( -+ this.registry, -+ RegistryEntry.Modifiable.asModifiable(this.entry), -+ resourceKey, -+ builder, -+ FROM_PLUGIN, -+ conversions -+ ); -+ } -+ -+ @Override -+ public final @Nullable T createBukkit(final NamespacedKey namespacedKey, final @Nullable M minecraft) { -+ if (minecraft == null) { -+ return null; -+ } -+ return this.minecraftToBukkit(namespacedKey, minecraft); -+ } -+ -+ public WritableRegistry createApiWritableRegistry(final Conversions conversions) { -+ return new ApiWritableRegistry(conversions); -+ } -+ -+ public T minecraftToBukkit(final NamespacedKey namespacedKey, final M minecraft) { -+ return this.minecraftToBukkit.apply(namespacedKey, minecraft); -+ } -+ -+ protected B newBuilder(final Conversions conversions, final TypedKey key) { -+ return this.builderFactory.create(conversions, key); -+ } -+ -+ public class ApiWritableRegistry implements WritableRegistry { -+ -+ private final Conversions conversions; -+ -+ public ApiWritableRegistry(final Conversions conversions) { -+ this.conversions = conversions; -+ } -+ -+ @Override -+ public void register(final TypedKey key, final Consumer value) { -+ WritableCraftRegistry.this.register(key, value, this.conversions); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/data/util/Conversions.java b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eda5cc7d45ef59ccc1c9c7e027c1f044f1dcc86b ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java -@@ -0,0 +1,36 @@ -+package io.papermc.paper.registry.data.util; -+ -+import com.mojang.serialization.JavaOps; -+import io.papermc.paper.adventure.WrapperAwareSerializer; -+import net.kyori.adventure.text.Component; -+import net.minecraft.resources.RegistryOps; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.Contract; -+ -+@DefaultQualifier(NonNull.class) -+public class Conversions { -+ -+ private final RegistryOps.RegistryInfoLookup lookup; -+ private final WrapperAwareSerializer serializer; -+ -+ public Conversions(final RegistryOps.RegistryInfoLookup lookup) { -+ this.lookup = lookup; -+ this.serializer = new WrapperAwareSerializer(() -> RegistryOps.create(JavaOps.INSTANCE, lookup)); -+ } -+ -+ public RegistryOps.RegistryInfoLookup lookup() { -+ return this.lookup; -+ } -+ -+ @Contract("null -> null; !null -> !null") -+ public net.minecraft.network.chat.@Nullable Component asVanilla(final @Nullable Component adventure) { -+ if (adventure == null) return null; -+ return this.serializer.serialize(adventure); -+ } -+ -+ public Component asAdventure(final net.minecraft.network.chat.@Nullable Component vanilla) { -+ return vanilla == null ? Component.empty() : this.serializer.deserialize(vanilla); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..aeec9b3ae2911f041d000b3db72f37974020ba60 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java -@@ -0,0 +1,44 @@ -+package io.papermc.paper.registry.entry; -+ -+import io.papermc.paper.registry.PaperRegistryBuilder; -+import io.papermc.paper.registry.RegistryHolder; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.WritableCraftRegistry; -+import io.papermc.paper.registry.data.util.Conversions; -+import java.util.function.BiFunction; -+import net.minecraft.core.MappedRegistry; -+import net.minecraft.core.Registry; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.Keyed; -+import org.bukkit.NamespacedKey; -+ -+public class AddableRegistryEntry> extends CraftRegistryEntry implements RegistryEntry.Addable { -+ -+ private final PaperRegistryBuilder.Filler builderFiller; -+ -+ protected AddableRegistryEntry( -+ final ResourceKey> mcKey, -+ final RegistryKey apiKey, -+ final Class classToPreload, -+ final BiFunction minecraftToBukkit, -+ final PaperRegistryBuilder.Filler builderFiller -+ ) { -+ super(mcKey, apiKey, classToPreload, minecraftToBukkit); -+ this.builderFiller = builderFiller; -+ } -+ -+ private WritableCraftRegistry createRegistry(final Registry registry) { -+ return new WritableCraftRegistry<>(this, this.classToPreload, (MappedRegistry) registry, this.updater, this.builderFiller.asFactory(), this.minecraftToBukkit); -+ } -+ -+ @Override -+ public RegistryHolder createRegistryHolder(final Registry nmsRegistry) { -+ return new RegistryHolder.Memoized<>(() -> this.createRegistry(nmsRegistry)); -+ } -+ -+ @Override -+ public B fillBuilder(final Conversions conversions, final TypedKey key, final M nms) { -+ return this.builderFiller.fill(conversions, key, nms); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..515a995e3862f8e7cb93d149315ea32e04a08716 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java -@@ -0,0 +1,32 @@ -+package io.papermc.paper.registry.entry; -+ -+import io.papermc.paper.registry.PaperRegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.data.util.Conversions; -+import java.util.function.BiFunction; -+import net.minecraft.core.Registry; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.Keyed; -+import org.bukkit.NamespacedKey; -+ -+public class ModifiableRegistryEntry> extends CraftRegistryEntry implements RegistryEntry.Modifiable { -+ -+ protected final PaperRegistryBuilder.Filler builderFiller; -+ -+ protected ModifiableRegistryEntry( -+ final ResourceKey> mcKey, -+ final RegistryKey apiKey, -+ final Class toPreload, -+ final BiFunction minecraftToBukkit, -+ final PaperRegistryBuilder.Filler builderFiller -+ ) { -+ super(mcKey, apiKey, toPreload, minecraftToBukkit); -+ this.builderFiller = builderFiller; -+ } -+ -+ @Override -+ public B fillBuilder(final Conversions conversions, final TypedKey key, final M nms) { -+ return this.builderFiller.fill(conversions, key, nms); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java -index 15991bf13894d850f360a520d1815711d25973ec..f2e919705301cb23ed1938ca3c1976378249172c 100644 ---- a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java -+++ b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java -@@ -1,7 +1,13 @@ - package io.papermc.paper.registry.entry; - -+import io.papermc.paper.registry.PaperRegistryBuilder; - import io.papermc.paper.registry.RegistryHolder; - import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.WritableCraftRegistry; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.event.RegistryEntryAddEventImpl; -+import io.papermc.paper.registry.event.RegistryFreezeEventImpl; - import io.papermc.paper.registry.legacy.DelayedRegistryEntry; - import java.util.function.BiFunction; - import java.util.function.Supplier; -@@ -11,6 +17,7 @@ import org.bukkit.Keyed; - import org.bukkit.NamespacedKey; - import org.bukkit.craftbukkit.util.ApiVersion; - import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; - import org.checkerframework.framework.qual.DefaultQualifier; - - @DefaultQualifier(NonNull.class) -@@ -32,6 +39,65 @@ public interface RegistryEntry extends RegistryEntryInfo(this); - } - -+ interface BuilderHolder> extends RegistryEntryInfo { -+ -+ B fillBuilder(Conversions conversions, TypedKey key, M nms); -+ } -+ -+ /** -+ * Can mutate values being added to the registry -+ */ -+ interface Modifiable> extends BuilderHolder { -+ -+ static boolean isModifiable(final @Nullable RegistryEntryInfo entry) { -+ return entry instanceof RegistryEntry.Modifiable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Modifiable); -+ } -+ -+ static > Modifiable asModifiable(final RegistryEntryInfo entry) { // TODO remove Keyed -+ return (Modifiable) possiblyUnwrap(entry); -+ } -+ -+ default RegistryEntryAddEventImpl createEntryAddEvent(final TypedKey key, final B initialBuilder, final Conversions conversions) { -+ return new RegistryEntryAddEventImpl<>(key, initialBuilder, this.apiKey(), conversions); -+ } -+ } -+ -+ /** -+ * Can only add new values to the registry, not modify any values. -+ */ -+ interface Addable> extends BuilderHolder { // TODO remove Keyed -+ -+ default RegistryFreezeEventImpl createFreezeEvent(final WritableCraftRegistry writableRegistry, final Conversions conversions) { -+ return new RegistryFreezeEventImpl<>(this.apiKey(), writableRegistry.createApiWritableRegistry(conversions), conversions); -+ } -+ -+ static boolean isAddable(final @Nullable RegistryEntryInfo entry) { -+ return entry instanceof RegistryEntry.Addable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Addable); -+ } -+ -+ static > Addable asAddable(final RegistryEntryInfo entry) { -+ return (Addable) possiblyUnwrap(entry); -+ } -+ } -+ -+ /** -+ * Can mutate values and add new values. -+ */ -+ interface Writable> extends Modifiable, Addable { // TODO remove Keyed -+ -+ static boolean isWritable(final @Nullable RegistryEntryInfo entry) { -+ return entry instanceof RegistryEntry.Writable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Writable); -+ } -+ -+ static > Writable asWritable(final RegistryEntryInfo entry) { // TODO remove Keyed -+ return (Writable) possiblyUnwrap(entry); -+ } -+ } -+ -+ private static RegistryEntryInfo possiblyUnwrap(final RegistryEntryInfo entry) { -+ return entry instanceof final DelayedRegistryEntry delayed ? delayed.delegate() : entry; -+ } -+ - static RegistryEntry entry( - final ResourceKey> mcKey, - final RegistryKey apiKey, -@@ -48,4 +114,24 @@ public interface RegistryEntry extends RegistryEntryInfo(mcKey, apiKey, apiRegistrySupplier); - } -+ -+ static > RegistryEntry modifiable( -+ final ResourceKey> mcKey, -+ final RegistryKey apiKey, -+ final Class toPreload, -+ final BiFunction minecraftToBukkit, -+ final PaperRegistryBuilder.Filler filler -+ ) { -+ return new ModifiableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler); -+ } -+ -+ static > RegistryEntry writable( -+ final ResourceKey> mcKey, -+ final RegistryKey apiKey, -+ final Class toPreload, -+ final BiFunction minecraftToBukkit, -+ final PaperRegistryBuilder.Filler filler -+ ) { -+ return new WritableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler); -+ } - } -diff --git a/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..562accce731630327d116afd1c9d559df7e386bd ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java -@@ -0,0 +1,22 @@ -+package io.papermc.paper.registry.entry; -+ -+import io.papermc.paper.registry.PaperRegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import java.util.function.BiFunction; -+import net.minecraft.core.Registry; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.Keyed; -+import org.bukkit.NamespacedKey; -+ -+public class WritableRegistryEntry> extends AddableRegistryEntry implements RegistryEntry.Writable { // TODO remove Keyed -+ -+ protected WritableRegistryEntry( -+ final ResourceKey> mcKey, -+ final RegistryKey apiKey, -+ final Class classToPreload, -+ final BiFunction minecraftToBukkit, -+ final PaperRegistryBuilder.Filler builderFiller -+ ) { -+ super(mcKey, apiKey, classToPreload, minecraftToBukkit, builderFiller); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cc9c8fd313f530777af80ad79e03903f3f8f9829 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java -@@ -0,0 +1,30 @@ -+package io.papermc.paper.registry.event; -+ -+import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; -+import io.papermc.paper.registry.PaperRegistries; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.set.NamedRegistryKeySetImpl; -+import io.papermc.paper.registry.tag.Tag; -+import io.papermc.paper.registry.tag.TagKey; -+import net.minecraft.core.HolderSet; -+import net.minecraft.resources.RegistryOps; -+import org.bukkit.Keyed; -+import org.checkerframework.checker.nullness.qual.NonNull; -+ -+public record RegistryEntryAddEventImpl>( -+ TypedKey key, -+ B builder, -+ RegistryKey registryKey, -+ Conversions conversions -+) implements RegistryEntryAddEvent, PaperLifecycleEvent { -+ -+ @Override -+ public @NonNull Tag getOrCreateTag(final TagKey tagKey) { -+ final RegistryOps.RegistryInfo registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow(); -+ final HolderSet.Named tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey)); -+ return new NamedRegistryKeySetImpl<>(tagKey, tagSet); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f5ea23173dcbe491742c3dd051c147ef397307a0 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java -@@ -0,0 +1,44 @@ -+package io.papermc.paper.registry.event; -+ -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; -+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import java.util.HashMap; -+import java.util.Map; -+import java.util.Objects; -+import java.util.function.BiFunction; -+ -+public final class RegistryEventMap { -+ -+ private final Map, LifecycleEventType, ?>> hooks = new HashMap<>(); -+ private final String name; -+ -+ public RegistryEventMap(final String name) { -+ this.name = name; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public , E extends RegistryEvent, ET extends LifecycleEventType> ET getOrCreate(final RegistryEventProvider type, final BiFunction, ? super String, ET> eventTypeCreator) { -+ final ET registerHook; -+ if (this.hooks.containsKey(type.registryKey())) { -+ registerHook = (ET) this.hooks.get(type.registryKey()); -+ } else { -+ registerHook = eventTypeCreator.apply(type, this.name); -+ LifecycleEventRunner.INSTANCE.addEventType(registerHook); -+ this.hooks.put(type.registryKey(), registerHook); -+ } -+ return registerHook; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public > LifecycleEventType getHook(final RegistryKey registryKey) { -+ return (LifecycleEventType) Objects.requireNonNull(this.hooks.get(registryKey), "No hook for " + registryKey); -+ } -+ -+ public boolean hasHooks(final RegistryKey registryKey) { -+ return this.hooks.containsKey(registryKey); -+ } -+ -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..34c842ffa355e3c8001dd7b8551bcb49229a6391 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java -@@ -0,0 +1,24 @@ -+package io.papermc.paper.registry.event; -+ -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; -+import io.papermc.paper.registry.PaperRegistryListenerManager; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.event.type.RegistryEntryAddEventType; -+ -+public class RegistryEventTypeProviderImpl implements RegistryEventTypeProvider { -+ -+ public static RegistryEventTypeProviderImpl instance() { -+ return (RegistryEventTypeProviderImpl) RegistryEventTypeProvider.provider(); -+ } -+ -+ @Override -+ public > RegistryEntryAddEventType registryEntryAdd(final RegistryEventProvider type) { -+ return PaperRegistryListenerManager.INSTANCE.getRegistryValueAddEventType(type); -+ } -+ -+ @Override -+ public > LifecycleEventType.Prioritizable> registryFreeze(final RegistryEventProvider type) { -+ return PaperRegistryListenerManager.INSTANCE.getRegistryFreezeEventType(type); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..63957d2509e68ccc6eb2fd9ecaa35bfad7b71b81 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java -@@ -0,0 +1,28 @@ -+package io.papermc.paper.registry.event; -+ -+import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; -+import io.papermc.paper.registry.PaperRegistries; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.set.NamedRegistryKeySetImpl; -+import io.papermc.paper.registry.tag.Tag; -+import io.papermc.paper.registry.tag.TagKey; -+import net.minecraft.core.HolderSet; -+import net.minecraft.resources.RegistryOps; -+import org.bukkit.Keyed; -+import org.checkerframework.checker.nullness.qual.NonNull; -+ -+public record RegistryFreezeEventImpl>( -+ RegistryKey registryKey, -+ WritableRegistry registry, -+ Conversions conversions -+) implements RegistryFreezeEvent, PaperLifecycleEvent { -+ -+ @Override -+ public @NonNull Tag getOrCreateTag(final TagKey tagKey) { -+ final RegistryOps.RegistryInfo registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow(); -+ final HolderSet.Named tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey)); -+ return new NamedRegistryKeySetImpl<>(tagKey, tagSet); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/package-info.java b/src/main/java/io/papermc/paper/registry/event/package-info.java -new file mode 100644 -index 0000000000000000000000000000000000000000..14d2d9766b8dee763f220c397aba3ad432d02aaa ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/package-info.java -@@ -0,0 +1,5 @@ -+@DefaultQualifier(NonNull.class) -+package io.papermc.paper.registry.event; -+ -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5d709ed04e1078b631f5b9c74ca35f042251e14f ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java -@@ -0,0 +1,32 @@ -+package io.papermc.paper.registry.event.type; -+ -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.event.RegistryEntryAddEvent; -+import io.papermc.paper.registry.event.RegistryEventProvider; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+ -+public class RegistryEntryAddEventTypeImpl> extends PrioritizableLifecycleEventType, RegistryEntryAddConfiguration> implements RegistryEntryAddEventType { -+ -+ public RegistryEntryAddEventTypeImpl(final RegistryEventProvider type, final String eventName) { -+ super(type.registryKey() + " / " + eventName, BootstrapContext.class); -+ } -+ -+ @Override -+ public RegistryEntryAddConfiguration newHandler(final LifecycleEventHandler> handler) { -+ return new RegistryEntryAddHandlerConfiguration<>(handler, this); -+ } -+ -+ @Override -+ public void forEachHandler(final RegistryEntryAddEvent event, final Consumer>> consumer, final Predicate>> predicate) { -+ super.forEachHandler(event, consumer, predicate.and(handler -> this.matchesTarget(event, handler))); -+ } -+ -+ private boolean matchesTarget(final RegistryEntryAddEvent event, final RegisteredHandler> handler) { -+ final RegistryEntryAddHandlerConfiguration config = (RegistryEntryAddHandlerConfiguration) handler.config(); -+ return config.filter() == null || config.filter().test(event.key()); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java -new file mode 100644 -index 0000000000000000000000000000000000000000..548f5bf979e88708e98d04dfe22ccaa300c91ddd ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java -@@ -0,0 +1,42 @@ -+package io.papermc.paper.registry.event.type; -+ -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl; -+import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.event.RegistryEntryAddEvent; -+import java.util.function.Predicate; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.jetbrains.annotations.Contract; -+ -+public class RegistryEntryAddHandlerConfiguration> extends PrioritizedLifecycleEventHandlerConfigurationImpl> implements RegistryEntryAddConfiguration { -+ -+ private @Nullable Predicate> filter; -+ -+ public RegistryEntryAddHandlerConfiguration(final LifecycleEventHandler> handler, final AbstractLifecycleEventType, ?> eventType) { -+ super(handler, eventType); -+ } -+ -+ @Contract(pure = true) -+ public @Nullable Predicate> filter() { -+ return this.filter; -+ } -+ -+ @Override -+ public RegistryEntryAddConfiguration filter(final Predicate> filter) { -+ this.filter = filter; -+ return this; -+ } -+ -+ @Override -+ public RegistryEntryAddConfiguration priority(final int priority) { -+ return (RegistryEntryAddConfiguration) super.priority(priority); -+ } -+ -+ @Override -+ public RegistryEntryAddConfiguration monitor() { -+ return (RegistryEntryAddConfiguration) super.monitor(); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dcc0f6b337840a78d38abdf2eb3f4bbd1676f58f ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java -@@ -0,0 +1,14 @@ -+package io.papermc.paper.registry.event.type; -+ -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType; -+import io.papermc.paper.registry.RegistryBuilder; -+import io.papermc.paper.registry.event.RegistryEvent; -+import io.papermc.paper.registry.event.RegistryEventProvider; -+ -+public final class RegistryLifecycleEventType, E extends RegistryEvent> extends PrioritizableLifecycleEventType.Simple { -+ -+ public RegistryLifecycleEventType(final RegistryEventProvider type, final String eventName) { -+ super(type.registryKey() + " / " + eventName, BootstrapContext.class); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java -index 5562e8da5ebaef2a3add46e88d64358b7737b59e..e5880f76cdb8ebf01fcefdf77ba9b95674b997a8 100644 ---- a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java -+++ b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java -@@ -1,12 +1,13 @@ - package io.papermc.paper.registry.legacy; - -+import io.papermc.paper.registry.tag.Tag; -+import io.papermc.paper.registry.tag.TagKey; - import java.util.Iterator; - import java.util.function.Supplier; - import java.util.stream.Stream; - import org.bukkit.Keyed; - import org.bukkit.NamespacedKey; - import org.bukkit.Registry; --import org.bukkit.craftbukkit.CraftRegistry; - import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - import org.checkerframework.checker.nullness.qual.Nullable; - import org.jetbrains.annotations.NotNull; -@@ -52,4 +53,14 @@ public final class DelayedRegistry> imple - public NamespacedKey getKey(final T value) { - return this.delegate().getKey(value); - } -+ -+ @Override -+ public boolean hasTag(final TagKey key) { -+ return this.delegate().hasTag(key); -+ } -+ -+ @Override -+ public @NotNull Tag getTag(final TagKey key) { -+ return this.delegate().getTag(key); -+ } - } -diff --git a/src/main/java/io/papermc/paper/registry/package-info.java b/src/main/java/io/papermc/paper/registry/package-info.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0b80179ff90e085568d7ceafd9b17511789dc99b ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/package-info.java -@@ -0,0 +1,5 @@ -+@DefaultQualifier(NonNull.class) -+package io.papermc.paper.registry; -+ -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -diff --git a/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e8c2c18a1ed5cd587266bd415170610781531a12 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java -@@ -0,0 +1,76 @@ -+package io.papermc.paper.registry.set; -+ -+import com.google.common.collect.ImmutableList; -+import com.google.common.collect.Iterables; -+import io.papermc.paper.registry.PaperRegistries; -+import io.papermc.paper.registry.RegistryAccess; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.tag.Tag; -+import io.papermc.paper.registry.tag.TagKey; -+import java.util.Collection; -+import java.util.Set; -+import net.kyori.adventure.key.Key; -+import net.minecraft.core.Holder; -+import net.minecraft.core.HolderSet; -+import org.bukkit.Keyed; -+import org.bukkit.NamespacedKey; -+import org.bukkit.Registry; -+import org.bukkit.craftbukkit.util.CraftNamespacedKey; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Unmodifiable; -+ -+@DefaultQualifier(NonNull.class) -+public record NamedRegistryKeySetImpl( // TODO remove Keyed -+ TagKey tagKey, -+ HolderSet.Named namedSet -+) implements Tag, org.bukkit.Tag { -+ -+ @Override -+ public @Unmodifiable Collection> values() { -+ final ImmutableList.Builder> builder = ImmutableList.builder(); -+ for (final Holder holder : this.namedSet) { -+ builder.add(TypedKey.create(this.tagKey.registryKey(), CraftNamespacedKey.fromMinecraft(((Holder.Reference) holder).key().location()))); -+ } -+ return builder.build(); -+ } -+ -+ @Override -+ public RegistryKey registryKey() { -+ return this.tagKey.registryKey(); -+ } -+ -+ @Override -+ public boolean contains(final TypedKey valueKey) { -+ return Iterables.any(this.namedSet, h -> { -+ return PaperRegistries.fromNms(((Holder.Reference) h).key()).equals(valueKey); -+ }); -+ } -+ -+ @Override -+ public @Unmodifiable Collection resolve(final Registry registry) { -+ final ImmutableList.Builder builder = ImmutableList.builder(); -+ for (final Holder holder : this.namedSet) { -+ builder.add(registry.getOrThrow(CraftNamespacedKey.fromMinecraft(((Holder.Reference) holder).key().location()))); -+ } -+ return builder.build(); -+ } -+ -+ @Override -+ public boolean isTagged(final T item) { -+ return this.getValues().contains(item); -+ } -+ -+ @Override -+ public Set getValues() { -+ return Set.copyOf(this.resolve(RegistryAccess.registryAccess().getRegistry(this.registryKey()))); -+ } -+ -+ @Override -+ public @NotNull NamespacedKey getKey() { -+ final Key key = this.tagKey().key(); -+ return new NamespacedKey(key.namespace(), key.value()); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f09ce9c8547ef05153847245746473dd9a8acbe6 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java -@@ -0,0 +1,48 @@ -+package io.papermc.paper.registry.set; -+ -+import io.papermc.paper.registry.PaperRegistries; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import java.util.ArrayList; -+import java.util.List; -+import net.minecraft.core.Holder; -+import net.minecraft.core.HolderSet; -+import net.minecraft.core.Registry; -+import net.minecraft.resources.RegistryOps; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.Keyed; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public final class PaperRegistrySets { -+ -+ public static HolderSet convertToNms(final ResourceKey> resourceKey, final RegistryOps.RegistryInfoLookup lookup, final RegistryKeySet registryKeySet) { // TODO remove Keyed -+ if (registryKeySet instanceof NamedRegistryKeySetImpl) { -+ return ((NamedRegistryKeySetImpl) registryKeySet).namedSet(); -+ } else { -+ final RegistryOps.RegistryInfo registryInfo = lookup.lookup(resourceKey).orElseThrow(); -+ return HolderSet.direct(key -> { -+ return registryInfo.getter().getOrThrow(PaperRegistries.toNms(key)); -+ }, registryKeySet.values()); -+ } -+ } -+ -+ public static RegistryKeySet convertToApi(final RegistryKey registryKey, final HolderSet holders) { // TODO remove Keyed -+ if (holders instanceof final HolderSet.Named named) { -+ return new NamedRegistryKeySetImpl<>(PaperRegistries.fromNms(named.key()), named); -+ } else { -+ final List> keys = new ArrayList<>(); -+ for (final Holder holder : holders) { -+ if (!(holder instanceof final Holder.Reference reference)) { -+ throw new UnsupportedOperationException("Cannot convert a holder set containing direct holders"); -+ } -+ keys.add(PaperRegistries.fromNms(reference.key())); -+ } -+ return RegistrySet.keySet(registryKey, keys); -+ } -+ } -+ -+ private PaperRegistrySets() { -+ } -+} -diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java -index edbbafd1705345282e5e6251eb71bfde5793b7d4..f22d22ebcedcc9c20225677844c86a1ad27c4211 100644 ---- a/src/main/java/net/minecraft/core/MappedRegistry.java -+++ b/src/main/java/net/minecraft/core/MappedRegistry.java -@@ -441,4 +441,12 @@ public class MappedRegistry implements WritableRegistry { - public HolderLookup.RegistryLookup asLookup() { - return this.lookup; - } -+ // Paper start -+ // used to clear intrusive holders from GameEvent, Item, Block, EntityType, and Fluid from unused instances of those types -+ public void clearIntrusiveHolder(final T instance) { -+ if (this.unregisteredIntrusiveHolders != null) { -+ this.unregisteredIntrusiveHolders.remove(instance); -+ } -+ } -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java -index 44b7927081b476813505cab6b3a2da2ec2942c54..0497318e8f647453f38f3a16a8be6bd9aa19253f 100644 ---- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java -+++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java -@@ -288,6 +288,17 @@ public class BuiltInRegistries { - Registries.ENCHANTMENT_PROVIDER_TYPE, EnchantmentProviderTypes::bootstrap - ); - public static final Registry> REGISTRY = WRITABLE_REGISTRY; -+ // Paper start - add built-in registry conversions -+ public static final io.papermc.paper.registry.data.util.Conversions BUILT_IN_CONVERSIONS = new io.papermc.paper.registry.data.util.Conversions(new net.minecraft.resources.RegistryOps.RegistryInfoLookup() { -+ @Override -+ public java.util.Optional> lookup(final ResourceKey> registryRef) { -+ final Registry registry = net.minecraft.server.RegistryLayer.STATIC_ACCESS.registryOrThrow(registryRef); -+ return java.util.Optional.of( -+ new net.minecraft.resources.RegistryOps.RegistryInfo<>(registry.asLookup(), registry.asTagAddingLookup(), Lifecycle.experimental()) -+ ); -+ } -+ }); -+ // Paper end - add built-in registry conversions - - private static Registry registerSimple(ResourceKey> key, BuiltInRegistries.RegistryBootstrap initializer) { - return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), initializer); -@@ -328,6 +339,7 @@ public class BuiltInRegistries { - } - public static void bootStrap(Runnable runnable) { - // Paper end -+ REGISTRY.freeze(); // Paper - freeze main registry early - createContents(); - runnable.run(); // Paper - freeze(); -@@ -346,6 +358,7 @@ public class BuiltInRegistries { - REGISTRY.freeze(); - - for (Registry registry : REGISTRY) { -+ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), BUILT_IN_CONVERSIONS); // Paper - registry.freeze(); - } - } -diff --git a/src/main/java/net/minecraft/resources/RegistryDataLoader.java b/src/main/java/net/minecraft/resources/RegistryDataLoader.java -index abadf4abe08dc3bb6612b42cbb3f7df3ffa28ce9..3053243866c655829fe2e980446b4abf1da6d37c 100644 ---- a/src/main/java/net/minecraft/resources/RegistryDataLoader.java -+++ b/src/main/java/net/minecraft/resources/RegistryDataLoader.java -@@ -115,7 +115,7 @@ public class RegistryDataLoader { - ); - - public static RegistryAccess.Frozen load(ResourceManager resourceManager, RegistryAccess registryManager, List> entries) { -- return load((loader, infoGetter) -> loader.loadFromResources(resourceManager, infoGetter), registryManager, entries); -+ return load((loader, infoGetter, conversions) -> loader.loadFromResources(resourceManager, infoGetter, conversions), registryManager, entries); // Paper - pass conversions - } - - public static RegistryAccess.Frozen load( -@@ -124,7 +124,7 @@ public class RegistryDataLoader { - RegistryAccess registryManager, - List> entries - ) { -- return load((loader, infoGetter) -> loader.loadFromNetwork(data, factory, infoGetter), registryManager, entries); -+ return load((loader, infoGetter, conversions) -> loader.loadFromNetwork(data, factory, infoGetter, conversions), registryManager, entries); // Paper - pass conversions - } - - private static RegistryAccess.Frozen load( -@@ -133,9 +133,11 @@ public class RegistryDataLoader { - Map, Exception> map = new HashMap<>(); - List> list = entries.stream().map(entry -> entry.create(Lifecycle.stable(), map)).collect(Collectors.toUnmodifiableList()); - RegistryOps.RegistryInfoLookup registryInfoLookup = createContext(baseRegistryManager, list); -- list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader)loader, registryInfoLookup)); -+ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryInfoLookup); // Paper - create conversions -+ list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader)loader, registryInfoLookup, conversions)); - list.forEach(loader -> { - Registry registry = loader.registry(); -+ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(loader.registry.key(), conversions); // Paper - run pre-freeze listeners - - try { - registry.freeze(); -@@ -193,13 +195,13 @@ public class RegistryDataLoader { - } - - private static void loadElementFromResource( -- WritableRegistry registry, Decoder decoder, RegistryOps ops, ResourceKey key, Resource resource, RegistrationInfo entryInfo -+ WritableRegistry registry, Decoder decoder, RegistryOps ops, ResourceKey key, Resource resource, RegistrationInfo entryInfo, io.papermc.paper.registry.data.util.Conversions conversions - ) throws IOException { - try (Reader reader = resource.openAsReader()) { - JsonElement jsonElement = JsonParser.parseReader(reader); - DataResult dataResult = decoder.parse(ops, jsonElement); - E object = dataResult.getOrThrow(); -- registry.register(key, object, entryInfo); -+ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(registry, key, object, entryInfo, conversions); // Paper - register with listeners - } - } - -@@ -208,7 +210,8 @@ public class RegistryDataLoader { - RegistryOps.RegistryInfoLookup infoGetter, - WritableRegistry registry, - Decoder elementDecoder, -- Map, Exception> errors -+ Map, Exception> errors, -+ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions - ) { - String string = Registries.elementsDirPath(registry.key()); - FileToIdConverter fileToIdConverter = FileToIdConverter.json(string); -@@ -221,7 +224,7 @@ public class RegistryDataLoader { - RegistrationInfo registrationInfo = REGISTRATION_INFO_CACHE.apply(resource.knownPackInfo()); - - try { -- loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo); -+ loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo, conversions); // Paper - pass conversions - } catch (Exception var15) { - errors.put( - resourceKey, -@@ -237,7 +240,8 @@ public class RegistryDataLoader { - RegistryOps.RegistryInfoLookup infoGetter, - WritableRegistry registry, - Decoder decoder, -- Map, Exception> loadingErrors -+ Map, Exception> loadingErrors, -+ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions - ) { - List list = data.get(registry.key()); - if (list != null) { -@@ -264,7 +268,7 @@ public class RegistryDataLoader { - - try { - Resource resource = factory.getResourceOrThrow(resourceLocation); -- loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO); -+ loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO, conversions); // Paper - pass conversions - } catch (Exception var18) { - loadingErrors.put(resourceKey, new IllegalStateException("Failed to parse local data", var18)); - } -@@ -274,22 +278,23 @@ public class RegistryDataLoader { - } - - static record Loader(RegistryDataLoader.RegistryData data, WritableRegistry registry, Map, Exception> loadingErrors) { -- public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter) { -- RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors); -+ public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions) { // Paper - pass conversions -+ RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions - } - - public void loadFromNetwork( - Map>, List> data, - ResourceProvider factory, -- RegistryOps.RegistryInfoLookup infoGetter -+ RegistryOps.RegistryInfoLookup infoGetter, -+ io.papermc.paper.registry.data.util.Conversions conversions // Paper - ) { -- RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors); -+ RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions - } - } - - @FunctionalInterface - interface LoadingFunction { -- void apply(RegistryDataLoader.Loader loader, RegistryOps.RegistryInfoLookup infoGetter); -+ void apply(RegistryDataLoader.Loader loader, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions); // Paper - pass conversions - } - - public static record RegistryData(ResourceKey> key, Codec elementCodec, boolean requiredNonEmpty) { -diff --git a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java -index 397bdacab9517354875ebc0bc68d35059b3c318b..908431652a0fea79b5a0cee1efd0c7a7d524b614 100644 ---- a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java -+++ b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java -@@ -47,15 +47,16 @@ public class ReloadableServerRegistries { - ) { - RegistryAccess.Frozen frozen = dynamicRegistries.getAccessForLoading(RegistryLayer.RELOADABLE); - RegistryOps registryOps = new ReloadableServerRegistries.EmptyTagLookupWrapper(frozen).createSerializationContext(JsonOps.INSTANCE); -+ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryOps.lookupProvider); // Paper - List>> list = LootDataType.values() -- .map(type -> scheduleElementParse((LootDataType)type, registryOps, resourceManager, prepareExecutor)) -+ .map(type -> scheduleElementParse((LootDataType)type, registryOps, resourceManager, prepareExecutor, conversions)) // Paper - .toList(); - CompletableFuture>> completableFuture = Util.sequence(list); - return completableFuture.thenApplyAsync(registries -> apply(dynamicRegistries, (List>)registries), prepareExecutor); - } - - private static CompletableFuture> scheduleElementParse( -- LootDataType type, RegistryOps ops, ResourceManager resourceManager, Executor prepareExecutor -+ LootDataType type, RegistryOps ops, ResourceManager resourceManager, Executor prepareExecutor, io.papermc.paper.registry.data.util.Conversions conversions // Paper - ) { - return CompletableFuture.supplyAsync( - () -> { -@@ -66,7 +67,7 @@ public class ReloadableServerRegistries { - SimpleJsonResourceReloadListener.scanDirectory(resourceManager, string, GSON, map); - map.forEach( - (id, json) -> type.deserialize(id, ops, json) -- .ifPresent(value -> writableRegistry.register(ResourceKey.create(type.registryKey(), id), (T)value, DEFAULT_REGISTRATION_INFO)) -+ .ifPresent(value -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, ResourceKey.create(type.registryKey(), id), value, DEFAULT_REGISTRATION_INFO, conversions)) // Paper - register with listeners - ); - return writableRegistry; - }, -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -index d21b7e39d71c785f47f790e1ad4be33a8e8e6e51..a47421425a8d5d2f07e08890fded0f7bfec4efb7 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -@@ -156,11 +156,11 @@ public class CraftRegistry implements Registry { - private final Map cache = new HashMap<>(); - private final Map byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry - private final net.minecraft.core.Registry minecraftRegistry; -- private final BiFunction minecraftToBukkit; -+ private final BiFunction minecraftToBukkit; // Paper - private final BiFunction serializationUpdater; // Paper - rename to make it *clear* what it is *only* for - private boolean init; - -- public CraftRegistry(Class bukkitClass, net.minecraft.core.Registry minecraftRegistry, BiFunction minecraftToBukkit, BiFunction serializationUpdater) { // Paper - relax preload class -+ public CraftRegistry(Class bukkitClass, net.minecraft.core.Registry minecraftRegistry, BiFunction minecraftToBukkit, BiFunction serializationUpdater) { // Paper - relax preload class - this.bukkitClass = bukkitClass; - this.minecraftRegistry = minecraftRegistry; - this.minecraftToBukkit = minecraftToBukkit; -@@ -233,4 +233,17 @@ public class CraftRegistry implements Registry { - return this.byValue.get(value); - } - // Paper end - improve Registry -+ -+ // Paper start - RegistrySet API -+ @Override -+ public boolean hasTag(final io.papermc.paper.registry.tag.TagKey key) { -+ return this.minecraftRegistry.getTag(net.minecraft.tags.TagKey.create(this.minecraftRegistry.key(), io.papermc.paper.adventure.PaperAdventure.asVanilla(key.key()))).isPresent(); -+ } -+ -+ @Override -+ public io.papermc.paper.registry.tag.Tag getTag(final io.papermc.paper.registry.tag.TagKey key) { -+ final net.minecraft.core.HolderSet.Named namedHolderSet = this.minecraftRegistry.getTag(io.papermc.paper.registry.PaperRegistries.toNms(key)).orElseThrow(); -+ return new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(key, namedHolderSet); -+ } -+ // Paper end - RegistrySet API - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 5a04134973dd1db7f778a57ec5f185feec370990..8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -685,6 +685,21 @@ public final class CraftMagicNumbers implements UnsafeValues { - } - // Paper end - lifecycle event API - -+ // Paper start - hack to get tags for non server-backed registries -+ @Override -+ public io.papermc.paper.registry.tag.Tag getTag(final io.papermc.paper.registry.tag.TagKey tagKey) { // TODO remove Keyed -+ if (tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.ENTITY_TYPE || tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.FLUID) { -+ throw new UnsupportedOperationException(tagKey.registryKey() + " doesn't have tags"); -+ } -+ final net.minecraft.resources.ResourceKey> nmsKey = io.papermc.paper.registry.PaperRegistries.registryToNms(tagKey.registryKey()); -+ final net.minecraft.core.Registry nmsRegistry = org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry().registryOrThrow(nmsKey); -+ return nmsRegistry -+ .getTag(io.papermc.paper.registry.PaperRegistries.toNms(tagKey)) -+ .map(named -> new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(tagKey, named)) -+ .orElse(null); -+ } -+ // Paper end - hack to get tags for non server-backed registries -+ - /** - * This helper class represents the different NBT Tags. - *

-diff --git a/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider -new file mode 100644 -index 0000000000000000000000000000000000000000..8bee1a5ed877a04e4d027593df1f42cefdd824e7 ---- /dev/null -+++ b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider -@@ -0,0 +1 @@ -+io.papermc.paper.registry.event.RegistryEventTypeProviderImpl -diff --git a/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f27e5e0037b719b1fc10703f8d298d2326b00432 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java -@@ -0,0 +1,34 @@ -+package io.papermc.paper.registry; -+ -+import io.papermc.paper.registry.data.util.Conversions; -+import java.util.List; -+import java.util.Map; -+import net.minecraft.core.Registry; -+import net.minecraft.resources.RegistryOps; -+import net.minecraft.resources.ResourceKey; -+import org.bukkit.support.AbstractTestingBase; -+import org.junit.jupiter.api.Disabled; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.Arguments; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+ -+class RegistryBuilderTest extends AbstractTestingBase { -+ -+ static List registries() { -+ return List.of( -+ ); -+ } -+ -+ @Disabled -+ @ParameterizedTest -+ @MethodSource("registries") -+ void testEquality(final ResourceKey> resourceKey, final PaperRegistryBuilder.Filler filler) { -+ final Registry registry = AbstractTestingBase.REGISTRY_CUSTOM.registryOrThrow(resourceKey); -+ for (final Map.Entry, M> entry : registry.entrySet()) { -+ final M built = filler.fill(new Conversions(new RegistryOps.HolderLookupAdapter(AbstractTestingBase.REGISTRY_CUSTOM)), PaperRegistries.fromNms(entry.getKey()), entry.getValue()).build(); -+ assertEquals(entry.getValue(), built); -+ } -+ } -+} diff --git a/patches/server/1022-Add-registry-entry-and-builders.patch b/patches/server/1022-Add-registry-entry-and-builders.patch deleted file mode 100644 index 1b920c14d0..0000000000 --- a/patches/server/1022-Add-registry-entry-and-builders.patch +++ /dev/null @@ -1,483 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Bjarne Koll -Date: Thu, 13 Jun 2024 23:45:32 +0200 -Subject: [PATCH] Add registry entry and builders - - -diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java -index a688af29273ebfbb4f75dd74cd30627fc481c96c..9c0972023bc9be20ba81bbc2e1d6688b615760c0 100644 ---- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java -+++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java -@@ -1,6 +1,8 @@ - package io.papermc.paper.registry; - - import io.papermc.paper.adventure.PaperAdventure; -+import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry; -+import io.papermc.paper.registry.data.PaperGameEventRegistryEntry; - import io.papermc.paper.registry.entry.RegistryEntry; - import io.papermc.paper.registry.tag.TagKey; - import java.util.Collections; -@@ -58,7 +60,7 @@ public final class PaperRegistries { - static { - REGISTRY_ENTRIES = List.of( - // built-ins -- entry(Registries.GAME_EVENT, RegistryKey.GAME_EVENT, GameEvent.class, CraftGameEvent::new), -+ writable(Registries.GAME_EVENT, RegistryKey.GAME_EVENT, GameEvent.class, CraftGameEvent::new, PaperGameEventRegistryEntry.PaperBuilder::new), - entry(Registries.INSTRUMENT, RegistryKey.INSTRUMENT, MusicInstrument.class, CraftMusicInstrument::new), - entry(Registries.MOB_EFFECT, RegistryKey.MOB_EFFECT, PotionEffectType.class, CraftPotionEffectType::new), - entry(Registries.STRUCTURE_TYPE, RegistryKey.STRUCTURE_TYPE, StructureType.class, CraftStructureType::new), -@@ -71,7 +73,7 @@ public final class PaperRegistries { - entry(Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN, TrimPattern.class, CraftTrimPattern::new).delayed(), - entry(Registries.DAMAGE_TYPE, RegistryKey.DAMAGE_TYPE, DamageType.class, CraftDamageType::new).delayed(), - entry(Registries.WOLF_VARIANT, RegistryKey.WOLF_VARIANT, Wolf.Variant.class, CraftWolf.CraftVariant::new).delayed(), -- entry(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT, Enchantment.class, CraftEnchantment::new).withSerializationUpdater(FieldRename.ENCHANTMENT_RENAME).delayed(), -+ writable(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT, Enchantment.class, CraftEnchantment::new, PaperEnchantmentRegistryEntry.PaperBuilder::new).withSerializationUpdater(FieldRename.ENCHANTMENT_RENAME).delayed(), - entry(Registries.JUKEBOX_SONG, RegistryKey.JUKEBOX_SONG, JukeboxSong.class, CraftJukeboxSong::new).delayed(), - - // api-only -diff --git a/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b9501554a0738776c265852184c7dbae3fe98c8c ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java -@@ -0,0 +1,234 @@ -+package io.papermc.paper.registry.data; -+ -+import com.google.common.base.Preconditions; -+import com.google.common.collect.Iterables; -+import com.google.common.collect.Lists; -+import io.papermc.paper.registry.PaperRegistryBuilder; -+import io.papermc.paper.registry.RegistryKey; -+import io.papermc.paper.registry.TypedKey; -+import io.papermc.paper.registry.data.util.Checks; -+import io.papermc.paper.registry.data.util.Conversions; -+import io.papermc.paper.registry.set.PaperRegistrySets; -+import io.papermc.paper.registry.set.RegistryKeySet; -+import java.util.Collections; -+import java.util.List; -+import java.util.Optional; -+import java.util.OptionalInt; -+import net.minecraft.core.HolderSet; -+import net.minecraft.core.component.DataComponentMap; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.network.chat.Component; -+import net.minecraft.world.entity.EquipmentSlotGroup; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.enchantment.Enchantment; -+import org.bukkit.craftbukkit.CraftEquipmentSlot; -+import org.bukkit.inventory.ItemType; -+import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.Range; -+ -+import static io.papermc.paper.registry.data.util.Checks.asArgument; -+import static io.papermc.paper.registry.data.util.Checks.asArgumentMin; -+import static io.papermc.paper.registry.data.util.Checks.asConfigured; -+ -+@DefaultQualifier(NonNull.class) -+public class PaperEnchantmentRegistryEntry implements EnchantmentRegistryEntry { -+ -+ // Top level -+ protected @MonotonicNonNull Component description; -+ -+ // Definition -+ protected @MonotonicNonNull HolderSet supportedItems; -+ protected @Nullable HolderSet primaryItems; -+ protected OptionalInt weight = OptionalInt.empty(); -+ protected OptionalInt maxLevel = OptionalInt.empty(); -+ protected Enchantment.@MonotonicNonNull Cost minimumCost; -+ protected Enchantment.@MonotonicNonNull Cost maximumCost; -+ protected OptionalInt anvilCost = OptionalInt.empty(); -+ protected @MonotonicNonNull List activeSlots; -+ -+ // Exclusive -+ protected HolderSet exclusiveWith = HolderSet.empty(); // Paper added default to empty. -+ -+ // Effects -+ protected final DataComponentMap effects; -+ -+ protected final Conversions conversions; -+ -+ public PaperEnchantmentRegistryEntry( -+ final Conversions conversions, -+ final TypedKey ignoredKey, -+ final @Nullable Enchantment internal -+ ) { -+ this.conversions = conversions; -+ if (internal == null) { -+ this.effects = DataComponentMap.EMPTY; -+ return; -+ } -+ -+ // top level -+ this.description = internal.description(); -+ -+ // definition -+ final Enchantment.EnchantmentDefinition definition = internal.definition(); -+ this.supportedItems = definition.supportedItems(); -+ this.primaryItems = definition.primaryItems().orElse(null); -+ this.weight = OptionalInt.of(definition.weight()); -+ this.maxLevel = OptionalInt.of(definition.maxLevel()); -+ this.minimumCost = definition.minCost(); -+ this.maximumCost = definition.maxCost(); -+ this.anvilCost = OptionalInt.of(definition.anvilCost()); -+ this.activeSlots = definition.slots(); -+ -+ // exclusive -+ this.exclusiveWith = internal.exclusiveSet(); -+ -+ // effects -+ this.effects = internal.effects(); -+ } -+ -+ @Override -+ public net.kyori.adventure.text.Component description() { -+ return this.conversions.asAdventure(asConfigured(this.description, "description")); -+ } -+ -+ @Override -+ public RegistryKeySet supportedItems() { -+ return PaperRegistrySets.convertToApi(RegistryKey.ITEM, asConfigured(this.supportedItems, "supportedItems")); -+ } -+ -+ @Override -+ public @Nullable RegistryKeySet primaryItems() { -+ return this.primaryItems == null ? null : PaperRegistrySets.convertToApi(RegistryKey.ITEM, this.primaryItems); -+ } -+ -+ @Override -+ public @Range(from = 1, to = 1024) int weight() { -+ return asConfigured(this.weight, "weight"); -+ } -+ -+ @Override -+ public @Range(from = 1, to = 255) int maxLevel() { -+ return asConfigured(this.maxLevel, "maxLevel"); -+ } -+ -+ @Override -+ public EnchantmentCost minimumCost() { -+ final Enchantment.@MonotonicNonNull Cost cost = asConfigured(this.minimumCost, "minimumCost"); -+ return EnchantmentRegistryEntry.EnchantmentCost.of(cost.base(), cost.perLevelAboveFirst()); -+ } -+ -+ @Override -+ public EnchantmentCost maximumCost() { -+ final Enchantment.@MonotonicNonNull Cost cost = asConfigured(this.maximumCost, "maximumCost"); -+ return EnchantmentRegistryEntry.EnchantmentCost.of(cost.base(), cost.perLevelAboveFirst()); -+ } -+ -+ @Override -+ public @Range(from = 0, to = Integer.MAX_VALUE) int anvilCost() { -+ return asConfigured(this.anvilCost, "anvilCost"); -+ } -+ -+ @Override -+ public List activeSlots() { -+ return Collections.unmodifiableList(Lists.transform(asConfigured(this.activeSlots, "activeSlots"), CraftEquipmentSlot::getSlot)); -+ } -+ -+ @Override -+ public RegistryKeySet exclusiveWith() { -+ return PaperRegistrySets.convertToApi(RegistryKey.ENCHANTMENT, this.exclusiveWith); -+ } -+ -+ public static final class PaperBuilder extends PaperEnchantmentRegistryEntry implements EnchantmentRegistryEntry.Builder, -+ PaperRegistryBuilder { -+ -+ public PaperBuilder(final Conversions conversions, final TypedKey key, final @Nullable Enchantment internal) { -+ super(conversions, key, internal); -+ } -+ -+ @Override -+ public Builder description(final net.kyori.adventure.text.Component description) { -+ this.description = this.conversions.asVanilla(asArgument(description, "description")); -+ return this; -+ } -+ -+ @Override -+ public Builder supportedItems(final RegistryKeySet supportedItems) { -+ this.supportedItems = PaperRegistrySets.convertToNms(Registries.ITEM, this.conversions.lookup(), asArgument(supportedItems, "supportedItems")); -+ return this; -+ } -+ -+ @Override -+ public Builder primaryItems(final @Nullable RegistryKeySet primaryItems) { -+ this.primaryItems = primaryItems == null ? null : PaperRegistrySets.convertToNms(Registries.ITEM, this.conversions.lookup(), primaryItems); -+ return this; -+ } -+ -+ @Override -+ public Builder weight(final @Range(from = 1, to = 1024) int weight) { -+ this.weight = OptionalInt.of(Checks.asArgumentRange(weight, "weight", 1, 1024)); -+ return this; -+ } -+ -+ @Override -+ public Builder maxLevel(final @Range(from = 1, to = 255) int maxLevel) { -+ this.maxLevel = OptionalInt.of(Checks.asArgumentRange(maxLevel, "maxLevel", 1, 255)); -+ return this; -+ } -+ -+ @Override -+ public Builder minimumCost(final EnchantmentCost minimumCost) { -+ final EnchantmentCost validCost = asArgument(minimumCost, "minimumCost"); -+ this.minimumCost = Enchantment.dynamicCost(validCost.baseCost(), validCost.additionalPerLevelCost()); -+ return this; -+ } -+ -+ @Override -+ public Builder maximumCost(final EnchantmentCost maximumCost) { -+ final EnchantmentCost validCost = asArgument(maximumCost, "maximumCost"); -+ this.maximumCost = Enchantment.dynamicCost(validCost.baseCost(), validCost.additionalPerLevelCost()); -+ return this; -+ } -+ -+ @Override -+ public Builder anvilCost(final @Range(from = 0, to = Integer.MAX_VALUE) int anvilCost) { -+ Preconditions.checkArgument(anvilCost >= 0, "anvilCost must be non-negative"); -+ this.anvilCost = OptionalInt.of(asArgumentMin(anvilCost, "anvilCost", 0)); -+ return this; -+ } -+ -+ @Override -+ public Builder activeSlots(final Iterable activeSlots) { -+ this.activeSlots = Lists.newArrayList(Iterables.transform(asArgument(activeSlots, "activeSlots"), CraftEquipmentSlot::getNMSGroup)); -+ return this; -+ } -+ -+ @Override -+ public Builder exclusiveWith(final RegistryKeySet exclusiveWith) { -+ this.exclusiveWith = PaperRegistrySets.convertToNms(Registries.ENCHANTMENT, this.conversions.lookup(), asArgument(exclusiveWith, "exclusiveWith")); -+ return this; -+ } -+ -+ @Override -+ public Enchantment build() { -+ final Enchantment.EnchantmentDefinition def = new Enchantment.EnchantmentDefinition( -+ asConfigured(this.supportedItems, "supportedItems"), -+ Optional.ofNullable(this.primaryItems), -+ this.weight(), -+ this.maxLevel(), -+ asConfigured(this.minimumCost, "minimumCost"), -+ asConfigured(this.maximumCost, "maximumCost"), -+ this.anvilCost(), -+ Collections.unmodifiableList(asConfigured(this.activeSlots, "activeSlots")) -+ ); -+ return new Enchantment( -+ asConfigured(this.description, "description"), -+ def, -+ this.exclusiveWith, -+ this.effects -+ ); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..18f9463ae23ba2d9c65ffb7531a87c925a5a8d6f ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java -@@ -0,0 +1,57 @@ -+package io.papermc.paper.registry.data; -+ -+import io.papermc.paper.registry.PaperRegistryBuilder; -+import io.papermc.paper.registry.data.util.Conversions; -+import java.util.OptionalInt; -+import net.minecraft.world.level.gameevent.GameEvent; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.Range; -+ -+import static io.papermc.paper.registry.data.util.Checks.asArgumentMin; -+import static io.papermc.paper.registry.data.util.Checks.asConfigured; -+ -+@DefaultQualifier(NonNull.class) -+public class PaperGameEventRegistryEntry implements GameEventRegistryEntry { -+ -+ protected OptionalInt range = OptionalInt.empty(); -+ -+ public PaperGameEventRegistryEntry( -+ final Conversions ignoredConversions, -+ final io.papermc.paper.registry.TypedKey ignoredKey, -+ final @Nullable GameEvent nms -+ ) { -+ if (nms == null) return; -+ -+ this.range = OptionalInt.of(nms.notificationRadius()); -+ } -+ -+ @Override -+ public @Range(from = 0, to = Integer.MAX_VALUE) int range() { -+ return asConfigured(this.range, "range"); -+ } -+ -+ public static final class PaperBuilder extends PaperGameEventRegistryEntry implements GameEventRegistryEntry.Builder, -+ PaperRegistryBuilder { -+ -+ public PaperBuilder( -+ final Conversions conversions, -+ final io.papermc.paper.registry.TypedKey key, -+ final @Nullable GameEvent nms -+ ) { -+ super(conversions, key, nms); -+ } -+ -+ @Override -+ public GameEventRegistryEntry.Builder range(final @Range(from = 0, to = Integer.MAX_VALUE) int range) { -+ this.range = OptionalInt.of(asArgumentMin(range, "range", 0)); -+ return this; -+ } -+ -+ @Override -+ public GameEvent build() { -+ return new GameEvent(this.range()); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/registry/data/util/Checks.java b/src/main/java/io/papermc/paper/registry/data/util/Checks.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a66cebe2b918a22b84e346127ffb671621050e5f ---- /dev/null -+++ b/src/main/java/io/papermc/paper/registry/data/util/Checks.java -@@ -0,0 +1,48 @@ -+package io.papermc.paper.registry.data.util; -+ -+import java.util.OptionalInt; -+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 Checks { -+ -+ public static T asConfigured(final @Nullable T value, final String field) { -+ if (value == null) { -+ throw new IllegalStateException(field + " has not been configured"); -+ } -+ return value; -+ } -+ -+ public static int asConfigured(final OptionalInt value, final String field) { -+ if (value.isEmpty()) { -+ throw new IllegalStateException(field + " has not been configured"); -+ } -+ return value.getAsInt(); -+ } -+ -+ public static T asArgument(final @Nullable T value, final String field) { -+ if (value == null) { -+ throw new IllegalArgumentException("argument " + value + " cannot be null"); -+ } -+ return value; -+ } -+ -+ public static int asArgumentRange(final int value, final String field, final int min, final int max) { -+ if (value < min || value > max) { -+ throw new IllegalArgumentException("argument " + field + " must be [" + min + ", " + max + "]"); -+ } -+ return value; -+ } -+ -+ public static int asArgumentMin(final int value, final String field, final int min) { -+ if (value < min) { -+ throw new IllegalArgumentException("argument " + field + " must be [" + min + ",+inf)"); -+ } -+ return value; -+ } -+ -+ private Checks() { -+ } -+} -diff --git a/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java b/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java -index cc3c56224e64816b885c0131ce2a800a2efe3113..7acd9c71c5c4b487e792b8c36a8e52e10b691e98 100644 ---- a/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java -+++ b/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java -@@ -85,7 +85,7 @@ public record GameEvent(int notificationRadius) { - } - - private static Holder.Reference register(String id, int range) { -- return Registry.registerForHolder(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(id), new GameEvent(range)); -+ return io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerForHolderWithListeners(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(id), new GameEvent(range)); // Paper - run with listeners - } - - public static record Context(@Nullable Entity sourceEntity, @Nullable BlockState affectedState) { -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java b/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java -index ac9b4328cd55a68664a3f71186bc9a7be7cd9658..ea9fe1f8b1a1685ea975eba0ca418a831006065a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java -@@ -18,10 +18,12 @@ public class CraftGameEvent extends GameEvent implements Handleable handleKey; // Paper - private final net.minecraft.world.level.gameevent.GameEvent handle; - - public CraftGameEvent(NamespacedKey key, net.minecraft.world.level.gameevent.GameEvent handle) { - this.key = key; -+ this.handleKey = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.GAME_EVENT, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(key)); // Paper - this.handle = handle; - } - -@@ -30,6 +32,18 @@ public class CraftGameEvent extends GameEvent implements Handleable registries() { - return List.of( -+ arguments(Registries.ENCHANTMENT, (PaperRegistryBuilder.Filler) PaperEnchantmentRegistryEntry.PaperBuilder::new), -+ arguments(Registries.GAME_EVENT, (PaperRegistryBuilder.Filler) PaperGameEventRegistryEntry.PaperBuilder::new) - ); - } - -- @Disabled - @ParameterizedTest - @MethodSource("registries") - void testEquality(final ResourceKey> resourceKey, final PaperRegistryBuilder.Filler filler) { diff --git a/patches/server/1022-Improve-performance-of-mass-crafts.patch b/patches/server/1022-Improve-performance-of-mass-crafts.patch new file mode 100644 index 0000000000..15701b21e0 --- /dev/null +++ b/patches/server/1022-Improve-performance-of-mass-crafts.patch @@ -0,0 +1,138 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 13 Aug 2023 15:41:52 -0700 +Subject: [PATCH] Improve performance of mass crafts + +When the server crafts all available items in CraftingMenu or InventoryMenu the game +checks either 4 or 9 times for each individual craft for a matching recipe for that container. +This check can be expensive if 64 total crafts are being performed with the recipe matching logic +being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching +recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. + +Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft +where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the +'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done +for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed +from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So +for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. + +After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the +initial recipe match. Then that recipe will be checked first for all future recipe match checks. + +diff --git a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java +index 779d107a4d07820529273af5931421c09d1dc27f..4f6c8c43f5150e340704682accfbe2a5b1c5db19 100644 +--- a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java +@@ -18,11 +18,11 @@ public interface CraftingContainer extends Container, StackedContentsCompatible + List getItems(); + + // CraftBukkit start +- default RecipeHolder getCurrentRecipe() { ++ default RecipeHolder getCurrentRecipe() { // Paper - use correct generic + return null; + } + +- default void setCurrentRecipe(RecipeHolder recipe) { ++ default void setCurrentRecipe(RecipeHolder recipe) { // Paper - use correct generic + } + // CraftBukkit end + +diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +index b572c905b2ef3cf79202c5f35eefd6a636e872f5..f9e4714cb0ee74505ed72200abed7756aa68dc90 100644 +--- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +@@ -79,6 +79,7 @@ public class CraftingMenu extends RecipeBookMenu + CraftingInput craftinginput = craftingInventory.asCraftInput(); + ServerPlayer entityplayer = (ServerPlayer) player; + ItemStack itemstack = ItemStack.EMPTY; ++ if (recipe == null) recipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first + Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftinginput, world, recipe); + craftingInventory.setCurrentRecipe(optional.orElse(null)); // CraftBukkit + +diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +index c75bc6bec06bb7c32eea81c0bc89441beb3d1150..37a89bf79017eb65f82276b054a70ddb5eb5e549 100644 +--- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java ++++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +@@ -63,7 +63,7 @@ public class ResultSlot extends Slot { + CraftingInput craftingInput = positioned.input(); + int i = positioned.left(); + int j = positioned.top(); +- NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, craftingInput, player.level()); ++ NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, craftingInput, player.level(), this.craftSlots.getCurrentRecipe()); // Paper - Perf: Improve mass crafting; check last recipe used first + + for (int k = 0; k < craftingInput.height(); k++) { + for (int l = 0; l < craftingInput.width(); l++) { +diff --git a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java +index 341e1f70602ecdb4782c9cd74fa19135ac230d90..42207462c4e720677a5ddbba4129dae60420aaa3 100644 +--- a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java +@@ -27,7 +27,7 @@ public class TransientCraftingContainer implements CraftingContainer { + + // CraftBukkit start - add fields + public List transaction = new java.util.ArrayList(); +- private RecipeHolder currentRecipe; ++ private RecipeHolder currentRecipe; // Paper - use correct generic + public Container resultInventory; + private Player owner; + private int maxStack = MAX_STACK; +@@ -72,12 +72,12 @@ public class TransientCraftingContainer implements CraftingContainer { + } + + @Override +- public RecipeHolder getCurrentRecipe() { ++ public RecipeHolder getCurrentRecipe() { // Paper - use correct generic + return this.currentRecipe; + } + + @Override +- public void setCurrentRecipe(RecipeHolder currentRecipe) { ++ public void setCurrentRecipe(RecipeHolder currentRecipe) { // Paper - use correct generic + this.currentRecipe = currentRecipe; + } + +diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +index 407f3c1938b5b5d893b09705fe4930dbdafa3c8e..de7c77c1b25680ecc65f0f43f2391aff269a974f 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java ++++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +@@ -111,13 +111,20 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { + } + + public > Optional> getRecipeFor(RecipeType type, I input, Level world, @Nullable RecipeHolder recipe) { +- // CraftBukkit start +- List> list = this.byType(type).stream().filter((recipeholder1) -> { +- return recipeholder1.value().matches(input, world); +- }).toList(); +- Optional> recipe1 = (list.isEmpty() || input.isEmpty()) ? Optional.empty() : (recipe != null && recipe.value().matches(input, world) ? Optional.of(recipe) : Optional.of(list.getLast())); // CraftBukkit - SPIGOT-4638: last recipe gets priority +- return recipe1; +- // CraftBukkit end ++ // Paper start - fix upstream's complete screw up of checking last used recipe ++ if (input.isEmpty()) { ++ return Optional.empty(); ++ } else if (recipe != null && recipe.value().matches(input, world)) { ++ return Optional.of(recipe); ++ } else { ++ // CraftBukkit start ++ List> list = this.byType(type).stream().filter((recipeholder1) -> { ++ return recipeholder1.value().matches(input, world); ++ }).toList(); ++ return list.isEmpty() ? Optional.empty() : Optional.of(list.getLast()); // CraftBukkit - SPIGOT-4638: last recipe gets priority ++ // CraftBukkit end ++ } ++ // Paper end + } + + public > List> getAllRecipesFor(RecipeType type) { +@@ -137,7 +144,12 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { + } + + public > NonNullList getRemainingItemsFor(RecipeType type, I input, Level world) { +- Optional> optional = this.getRecipeFor(type, input, world); ++ // Paper start - Perf: improve performance of mass crafts ++ return this.getRemainingItemsFor(type, input, world, null); ++ } ++ public > NonNullList getRemainingItemsFor(RecipeType type, I input, Level world, @Nullable RecipeHolder previousRecipe) { ++ Optional> optional = this.getRecipeFor(type, input, world, previousRecipe); ++ // Paper end - Perf: improve performance of mass crafts + + if (optional.isPresent()) { + return ((RecipeHolder) optional.get()).value().getRemainingItems(input); diff --git a/patches/server/1023-Improved-Watchdog-Support.patch b/patches/server/1023-Improved-Watchdog-Support.patch deleted file mode 100644 index 6c5e71ba44..0000000000 --- a/patches/server/1023-Improved-Watchdog-Support.patch +++ /dev/null @@ -1,433 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sun, 12 Apr 2020 15:50:48 -0400 -Subject: [PATCH] Improved Watchdog Support - -Forced Watchdog Crash support and Improve Async Shutdown - -If the request to shut down the server is received while we are in -a watchdog hang, immediately treat it as a crash and begin the shutdown -process. Shutdown process is now improved to also shutdown cleanly when -not using restart scripts either. - -If a server is deadlocked, a server owner can send SIGUP (or any other signal -the JVM understands to shut down as it currently does) and the watchdog -will no longer need to wait until the full timeout, allowing you to trigger -a close process and try to shut the server down gracefully, saving player and -world data. - -Previously there was no way to trigger this outside of waiting for a full watchdog -timeout, which may be set to a really long time... - -Additionally, fix everything to do with shutting the server down asynchronously. - -Previously, nearly everything about the process was fragile and unsafe. Main might -not have actually been frozen, and might still be manipulating state. - -Or, some reuest might ask main to do something in the shutdown but main is dead. - -Or worse, other things might start closing down items such as the Console or Thread Pool -before we are fully shutdown. - -This change tries to resolve all of these issues by moving everything into the stop -method and guaranteeing only one thread is stopping the server. - -We then issue Thread Death to the main thread of another thread initiates the stop process. -We have to ensure Thread Death propagates correctly though to stop main completely. - -This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. - -This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they -are properly accounted for and wont trip watchdog on init. - -diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 ---- a/src/main/java/com/destroystokyo/paper/Metrics.java -+++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -92,7 +92,12 @@ public class Metrics { - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { -- final Runnable submitTask = this::submitData; -+ final Runnable submitTask = () -> { -+ if (MinecraftServer.getServer().hasStopped()) { -+ return; -+ } -+ 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. -diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index 589a8bf75be6ccc59f1e5dd5d8d9afed41c4772d..b24265573fdef5d9a964bcd76146f34542c420cf 100644 ---- a/src/main/java/net/minecraft/CrashReport.java -+++ b/src/main/java/net/minecraft/CrashReport.java -@@ -237,6 +237,7 @@ public class CrashReport { - } - - public static CrashReport forThrowable(Throwable cause, String title) { -+ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper - while (cause instanceof CompletionException && cause.getCause() != null) { - cause = cause.getCause(); - } -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 17b0e570016504d1b7704bbfa9ff2e3233b45d09..330bee331335454a61cf8350a6654217c8124445 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 processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); - public int autosavePeriod; - // Paper - don't store the vanilla dispatcher -- private boolean forceTicks; -+ public boolean forceTicks; // Paper - Improved watchdog support - // CraftBukkit end - // Spigot start - public static final int TPS = 20; -@@ -320,6 +320,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system -@@ -1005,6 +1008,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; -+ } -+ // Paper end - return new TickTask(this.tickCount, runnable); - } - -@@ -2267,7 +2319,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler - try { - task.run(); - } catch (Exception var3) { -+ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper - LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", this.name(), var3); - } - } -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 06e9fc703b0ef722cc21a365203a561009047d23..fa6b8ce6505a3c094187298a63b0e944e1e74af1 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -992,6 +992,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - try { - tickConsumer.accept(entity); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // 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); -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 b2a06fc1192cd6050d6d7ea8620a4fa5a12182cc..d388fbcbff63928f0e9140c02400a63ba8f19d9c 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -1006,6 +1006,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - gameprofilerfiller.pop(); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // 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); -diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { - @Override - public void run() { - try { -+ // Paper start - try to shutdown on main -+ server.safeShutdown(false, false); -+ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { -+ Thread.sleep(100); -+ } -+ if (server.hasStopped()) { -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ return; -+ } -+ // Looks stalled, close async - org.spigotmc.AsyncCatcher.enabled = false; // Spigot -+ server.forceTicks = true; - this.server.close(); -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ // Paper end - } finally { -+ org.apache.logging.log4j.LogManager.shutdown(); // Paper - try { -- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender -+ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop - } catch (Exception e) { - } - } -diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java -index f3fa0340babfc5eb627066115164e2d885c602c2..9ce945dce3ac8aa1d4eb55c41115f989b985d46d 100644 ---- a/src/main/java/org/spigotmc/RestartCommand.java -+++ b/src/main/java/org/spigotmc/RestartCommand.java -@@ -139,7 +139,7 @@ public class RestartCommand extends Command - // Paper end - - // Paper start - copied from above and modified to return if the hook registered -- private static boolean addShutdownHook(String restartScript) -+ public static boolean addShutdownHook(String restartScript) // Paper - { - String[] split = restartScript.split( " " ); - if ( split.length > 0 && new File( split[0] ).isFile() ) -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index 7507e3058e7519a3e13b3be061746151a71b8f20..e5e41dc2d4f7a8c3fea704212507ca0b951664db 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; - public class WatchdogThread extends Thread - { - -+ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support - private static WatchdogThread instance; - private long timeoutTime; - private boolean restart; -@@ -39,6 +40,7 @@ public class WatchdogThread extends Thread - { - if ( WatchdogThread.instance == null ) - { -+ if (timeoutTime <= 0) timeoutTime = 300; // Paper - WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); - WatchdogThread.instance.start(); - } else -@@ -70,12 +72,13 @@ public class WatchdogThread extends Thread - // Paper start - Logger log = Bukkit.getServer().getLogger(); - long currentTime = WatchdogThread.monotonicMillis(); -- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable -+ MinecraftServer server = MinecraftServer.getServer(); -+ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable - { -- boolean isLongTimeout = currentTime > lastTick + timeoutTime; -+ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); - // Don't spam early warning dumps - if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; -- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... -+ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... - lastEarlyWarning = currentTime; - if (isLongTimeout) { - // Paper end -@@ -136,9 +139,24 @@ public class WatchdogThread extends Thread - - if ( isLongTimeout ) - { -- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) -+ if ( !server.hasStopped() ) - { -- RestartCommand.restart(); -+ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us -+ server.forceTicks = true; -+ if (restart) { -+ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); -+ } -+ // try one last chance to safe shutdown on main incase it 'comes back' -+ server.abnormalExit = true; -+ server.safeShutdown(false, restart); -+ try { -+ Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ } -+ if (!server.hasStopped()) { -+ server.close(); -+ } - } - break; - } // Paper end -diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml -index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 ---- a/src/main/resources/log4j2.xml -+++ b/src/main/resources/log4j2.xml -@@ -1,5 +1,5 @@ - -- -+ - - - diff --git a/patches/server/1023-Properly-resend-entities.patch b/patches/server/1023-Properly-resend-entities.patch new file mode 100644 index 0000000000..398462967a --- /dev/null +++ b/patches/server/1023-Properly-resend-entities.patch @@ -0,0 +1,232 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Wed, 7 Dec 2022 17:25:19 -0500 +Subject: [PATCH] Properly resend entities + +This resolves some issues which caused entities to not be resent correctly. +Entities that are interacted with need to be resent to the client, so we resend all the entity +data to the player whilst making sure not to clear dirty entries from the tracker. This makes +sure that values will be correctly updated to other players. + +This also adds utilities to aid in further preventing entity desyncs. + +This also also fixes the bug causing cancelling PlayerInteractEvent to cause items to continue +to be used despite being cancelled on the server. + +For example, items being consumed but never finishing, shields being put up, etc. +The underlying issue of this is that the client modifies their synced data values, +and so we have to (forcibly) resend them in order for the client to reset their using item state. + +See: https://github.com/PaperMC/Paper/pull/1896 + +== AT == +public net.minecraft.server.level.ChunkMap$TrackedEntity serverEntity + +diff --git a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java +index 02bf2705ca1c99023a83a22d92e1962181102297..0f99733660f91280e4c6262cf75b3c9cae86f65a 100644 +--- a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java ++++ b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java +@@ -50,7 +50,7 @@ public class SynchedEntityData { + } + } + +- private SynchedEntityData.DataItem getItem(EntityDataAccessor key) { ++ public SynchedEntityData.DataItem getItem(EntityDataAccessor key) { // Paper - public + return (SynchedEntityData.DataItem) this.itemsById[key.id()]; // CraftBukkit - decompile error + } + +@@ -151,6 +151,20 @@ public class SynchedEntityData { + } + } + ++ // Paper start ++ // We need to pack all as we cannot rely on "non default values" or "dirty" ones. ++ // Because these values can possibly be desynced on the client. ++ @Nullable ++ public List> packAll() { ++ final List> list = new ArrayList<>(); ++ for (final DataItem dataItem : this.itemsById) { ++ list.add(dataItem.value()); ++ } ++ ++ return list; ++ } ++ // Paper end ++ + public static class DataItem { + + final EntityDataAccessor accessor; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index e9dcdb1e09e84a9b451034ff4bdfa6eae2dd1c04..24b1715397ba8e6f5e9841a030d0e3d964356f89 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -561,6 +561,7 @@ public class ServerPlayerGameMode { + } + // Paper end - extend Player Interact cancellation + player.getBukkitEntity().updateInventory(); // SPIGOT-2867 ++ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items + return (event.useItemInHand() != Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS; + } else if (this.gameModeForPlayer == GameType.SPECTATOR) { + MenuProvider itileinventory = iblockdata.getMenuProvider(world, blockposition); +@@ -612,6 +613,11 @@ public class ServerPlayerGameMode { + + return enuminteractionresult; + } else { ++ // Paper start - Properly cancel usable items; Cancel only if cancelled + if the interact result is different from default response ++ if (this.interactResult && this.interactResult != cancelledItem) { ++ this.player.resyncUsingItem(this.player); ++ } ++ // Paper end - Properly cancel usable items + return InteractionResult.PASS; + } + } +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index a8debfad8c8e66099f8a9aedc6f1971a8576dade..7796e191747be545e744564a2b0b65790f69114d 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -1949,6 +1949,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + if (cancelled) { ++ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items + this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524 + return; + } +@@ -2718,7 +2719,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a + if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { +- entity.getBukkitEntity().update(ServerGamePacketListenerImpl.this.player); ++ entity.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it + ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 14fb62dedabf2d858bea1877c2e28d1a7b6f5be4..bad83045ff9409678a0699cdc0781efd8ef30a44 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -393,7 +393,7 @@ public abstract class PlayerList { + ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now + // CraftBukkit end + +- player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn ++ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE + + this.sendLevelInfo(player, worldserver1); + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index f1fb4e830c6720d09b22056e3d0b9a08fe2bd472..83f3ffdd8fa901b3de580d2359cdb5ead0d762cb 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -665,13 +665,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + // CraftBukkit start + public void refreshEntityData(ServerPlayer to) { +- List> list = this.getEntityData().getNonDefaultValues(); ++ List> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default + +- if (list != null) { ++ if (list != null && to.getBukkitEntity().canSee(this.getBukkitEntity())) { // Paper + to.connection.send(new ClientboundSetEntityDataPacket(this.getId(), list)); + } + } + // CraftBukkit end ++ // Paper start ++ // This method should only be used if the data of an entity could have become desynced ++ // due to interactions on the client. ++ public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { ++ if (this.tracker == null) { ++ return; ++ } ++ ++ if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { ++ final net.minecraft.server.level.ServerEntity serverEntity = this.tracker.serverEntity; ++ final List> list = new java.util.ArrayList<>(); ++ serverEntity.sendPairingData(player, list::add); ++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(list)); ++ } ++ } ++ ++ // This method allows you to specifically resend certain data accessor keys to the client ++ public void resendPossiblyDesyncedDataValues(List> keys, ServerPlayer to) { ++ if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) { ++ return; ++ } ++ ++ final List> values = new java.util.ArrayList<>(keys.size()); ++ for (final EntityDataAccessor key : keys) { ++ final SynchedEntityData.DataItem synchedValue = this.entityData.getItem(key); ++ values.add(synchedValue.value()); ++ } ++ ++ to.connection.send(new ClientboundSetEntityDataPacket(this.id, values)); ++ } ++ // Paper end + + public boolean equals(Object object) { + return object instanceof Entity ? ((Entity) object).id == this.id : false; +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index affc39430e1f77e09768d2f614fcd5073d5a9270..b9cef93fe382b666bec04ca95eeaf2d8acbb3c40 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -3845,6 +3845,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; + } + ++ // Paper start - Properly cancel usable items ++ public void resyncUsingItem(ServerPlayer serverPlayer) { ++ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); ++ } ++ // Paper end - Properly cancel usable items + private void updatingUsingItem() { + if (this.isUsingItem()) { + if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java +index b586116d8cca1585f9c9e618ed4d0cb2ef2747be..acf38ef6d8de8b15cf2b09eb7bda390c4e446e9a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java +@@ -108,8 +108,7 @@ public interface Bucketable { + itemstack1 = CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket()); + if (playerBucketFishEvent.isCancelled()) { + ((ServerPlayer) player).containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket +- entity.getBukkitEntity().update((ServerPlayer) player); // We need to play out these packets as the client assumes the fish is gone +- entity.refreshEntityData((ServerPlayer) player); // Need to send data such as the display name to client ++ entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper + return Optional.of(InteractionResult.FAIL); + } + entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index b8eb9166e44da8745a056bf68f2f9316ce25d7a7..2cde808bfa797256409879505ba205a71f381981 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -1012,7 +1012,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return; + } + +- entityTracker.broadcast(this.getHandle().getAddEntityPacket(entityTracker.serverEntity)); ++ // Paper start - resend possibly desynced entity instead of add entity packet ++ for (final ServerPlayerConnection connection : entityTracker.seenBy) { ++ this.getHandle().resendPossiblyDesyncedEntityData(connection.getPlayer()); ++ } ++ // Paper end - resend possibly desynced entity instead of add entity packet + } + + public void update(ServerPlayer player) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +index f3a9b3380246fb2dd4b60a8d1a94c5dfed98d316..350ad61ab3fe66abd528e353b431a4a6dac17506 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +@@ -39,9 +39,11 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame { + protected void update() { + super.update(); + ++ // Paper start, don't mark as dirty as this is handled in super.update() + // mark dirty, so that the client gets updated with item and rotation +- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); +- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); ++ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); ++ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); ++ // Paper end + + // update redstone + if (!this.getHandle().generation) { diff --git a/patches/server/1024-Optimise-random-block-ticking.patch b/patches/server/1024-Optimise-random-block-ticking.patch new file mode 100644 index 0000000000..774d250ea6 --- /dev/null +++ b/patches/server/1024-Optimise-random-block-ticking.patch @@ -0,0 +1,456 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 20 Jun 2021 16:19:26 -0700 +Subject: [PATCH] Optimise random block ticking + +Massive performance improvement for random block ticking. +The performance increase comes from the fact that the vast +majority of attempted block ticks (~95% in my testing) fail +because the randomly selected block is not tickable. + +Now only tickable blocks are targeted, however this means that +the maximum number of block ticks occurs per chunk. However, +not all chunks are going to be targeted. The percent chance +of a chunk being targeted is based on how many tickable blocks +are in the chunk. +This means that while block ticks are spread out less, the +total number of blocks ticked per world tick remains the same. +Therefore, the chance of a random tickable block being ticked +remains the same. + +diff --git a/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d93652c1abbb6aee6eb7c26cf35d4d032ef7b69 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.util.math; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.LegacyRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class ThreadUnsafeRandom extends LegacyRandomSource { ++ ++ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them. ++ private static final long multiplier = 0x5DEECE66DL; ++ private static final long addend = 0xBL; ++ private static final long mask = (1L << 48) - 1; ++ ++ private static long initialScramble(long seed) { ++ return (seed ^ multiplier) & mask; ++ } ++ ++ private long seed; ++ ++ public ThreadUnsafeRandom(long seed) { ++ super(seed); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return new ThreadUnsafeRandom(this.nextLong()); ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setSeed(long seed) { ++ // note: called by Random constructor ++ this.seed = initialScramble(seed); ++ } ++ ++ @Override ++ public int next(int bits) { ++ // avoid the expensive CAS logic used by superclass ++ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits)); ++ } ++ ++ // Taken from ++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ ++ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c ++ // Original license is public domain ++ public static int fastRandomBounded(final long randomInteger, final long limit) { ++ // randomInteger must be [0, pow(2, 32)) ++ // limit must be [0, pow(2, 32)) ++ return (int)((randomInteger * limit) >>> 32); ++ } ++ ++ @Override ++ public int nextInt(int bound) { ++ // yes this breaks random's spec ++ // however there's nothing that uses this class that relies on it ++ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 2d97ca1f3c625c0206d35b785c57d9587924ed0a..e079f4db4e4738f60a6fdbdbf5e4d1baf593a62f 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -813,6 +813,10 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + entityplayer.stopSleepInBed(false, false); + }); + } ++ // Paper start - optimise random block ticking ++ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); ++ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); ++ // Paper end + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + ChunkPos chunkcoordintpair = chunk.getPos(); +@@ -822,8 +826,10 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); ++ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change ++ + 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)); ++ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper + + if (this.isRainingAt(blockposition)) { + DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); +@@ -855,7 +861,10 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + 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 start ++ this.getRandomBlockPosition(j, 0, k, 15, blockposition); ++ this.tickPrecipitation(blockposition, chunk); ++ // Paper end + } + } + } // Paper - Option to disable ice and snow +@@ -863,36 +872,37 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + gameprofilerfiller.popPush("tickBlocks"); + timings.chunkTicksBlocks.startTiming(); // Paper + if (randomTickSpeed > 0) { +- LevelChunkSection[] achunksection = chunk.getSections(); +- +- for (int i1 = 0; i1 < achunksection.length; ++i1) { +- LevelChunkSection chunksection = achunksection[i1]; +- +- if (chunksection.isRandomlyTicking()) { +- int j1 = chunk.getSectionYFromSectionIndex(i1); +- int k1 = SectionPos.sectionToBlockCoord(j1); +- +- for (int l1 = 0; l1 < randomTickSpeed; ++l1) { +- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15); +- +- gameprofilerfiller.push("randomTick"); +- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k); +- +- if (iblockdata.isRandomlyTicking()) { +- iblockdata.randomTick(this, blockposition1, this.random); +- } ++ // Paper start - optimize random block ticking ++ LevelChunkSection[] sections = chunk.getSections(); ++ final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); ++ for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { ++ LevelChunkSection section = sections[sectionIndex]; ++ if (section == null || section.tickingList.size() == 0) continue; ++ ++ int yPos = (sectionIndex + minSection) << 4; ++ for (int a = 0; a < randomTickSpeed; ++a) { ++ int tickingBlocks = section.tickingList.size(); ++ int index = this.randomTickRandom.nextInt(16 * 16 * 16); ++ if (index >= tickingBlocks) { ++ continue; ++ } + +- FluidState fluid = iblockdata.getFluidState(); ++ long raw = section.tickingList.getRaw(index); ++ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); ++ int randomX = location & 15; ++ int randomY = ((location >>> (4 + 4)) & 255) | yPos; ++ int randomZ = (location >>> 4) & 15; + +- if (fluid.isRandomlyTicking()) { +- fluid.randomTick(this, blockposition1, this.random); +- } ++ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); ++ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); + +- gameprofilerfiller.pop(); +- } ++ iblockdata.randomTick(this, blockposition2, this.randomTickRandom); + } ++ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). ++ // TODO CHECK ON UPDATE (ping the Canadian) + } + } ++ // Paper end - optimise random block ticking + + timings.chunkTicksBlocks.stopTiming(); // Paper + gameprofilerfiller.pop(); +@@ -900,17 +910,25 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + + @VisibleForTesting + public void tickPrecipitation(BlockPos pos) { +- BlockPos blockposition1 = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos); +- BlockPos blockposition2 = blockposition1.below(); ++ // Paper start - optimise chunk ticking ++ tickPrecipitation(pos.mutable(), this.getChunkAt(pos)); ++ } ++ public void tickPrecipitation(BlockPos.MutableBlockPos blockposition1, final LevelChunk chunk) { ++ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition1.getX() & 15, blockposition1.getZ() & 15) + 1; ++ int downY = normalY - 1; ++ blockposition1.setY(normalY); ++ // Paper end - optimise chunk ticking + Biome biomebase = (Biome) this.getBiome(blockposition1).value(); + +- if (biomebase.shouldFreeze(this, blockposition2)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition2, Blocks.ICE.defaultBlockState(), null); // CraftBukkit ++ blockposition1.setY(downY); ++ if (biomebase.shouldFreeze(this, blockposition1)) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit + } + + if (this.isRaining()) { + int i = this.getGameRules().getInt(GameRules.RULE_SNOW_ACCUMULATION_HEIGHT); + ++ blockposition1.setY(normalY); // Paper - optimise chunk ticking + if (i > 0 && biomebase.shouldSnow(this, blockposition1)) { + BlockState iblockdata = this.getBlockState(blockposition1); + +@@ -928,12 +946,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + } + } + +- Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition2); ++ blockposition1.setY(downY); // Paper - optimise chunk ticking ++ Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition1); // Paper - optimise chunk ticking + + if (biomebase_precipitation != Biome.Precipitation.NONE) { +- BlockState iblockdata2 = this.getBlockState(blockposition2); ++ BlockState iblockdata2 = this.getBlockState(blockposition1); // Paper - optimise chunk ticking + +- iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition2, biomebase_precipitation); ++ iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition1, biomebase_precipitation); // Paper - optimise chunk ticking + } + } + +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 68648c5a5e3ff079f832092af0f2f801c42d1ede..8bafd5fd7499ba4a04bf706cfd1e156073716e21 100644 +--- a/src/main/java/net/minecraft/util/BitStorage.java ++++ b/src/main/java/net/minecraft/util/BitStorage.java +@@ -20,4 +20,15 @@ public interface BitStorage { + void unpack(int[] out); + + BitStorage copy(); ++ ++ // Paper start ++ void forEach(DataBitConsumer consumer); ++ ++ @FunctionalInterface ++ interface DataBitConsumer { ++ ++ void accept(int location, int data); ++ ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8d7d763bf51cac556057645e6169c9447993189b 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -315,6 +315,28 @@ public class SimpleBitStorage implements BitStorage { + return this.bits; + } + ++ // Paper start ++ @Override ++ public final void forEach(DataBitConsumer consumer) { ++ int i = 0; ++ long[] along = this.data; ++ int j = along.length; ++ ++ for (int k = 0; k < j; ++k) { ++ long l = along[k]; ++ ++ for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { ++ consumer.accept(i, (int) (l & this.mask)); ++ l >>= this.bits; ++ ++i; ++ if (i >= this.size) { ++ return; ++ } ++ } ++ } ++ } ++ // Paper end ++ + @Override + public void getAll(IntConsumer action) { + int i = 0; +diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java +index 50040c497a819cd1229042ab3cb057d34a32cacc..01f5b946fabbe34f31110e75973dab9f39897346 100644 +--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java ++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java +@@ -46,6 +46,15 @@ public class ZeroBitStorage implements BitStorage { + return 0; + } + ++ // Paper start ++ @Override ++ public void forEach(DataBitConsumer consumer) { ++ for(int i = 0; i < this.size; ++i) { ++ consumer.accept(i, 0); ++ } ++ } ++ // Paper end ++ + @Override + public void getAll(IntConsumer action) { + for (int i = 0; i < this.size; i++) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 4bfa947531c4a67989e18032754dabf4c69e989c..caf4120721be8f2f7e2d737abbf73296cbe170b5 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -88,7 +88,7 @@ public class Turtle extends Animal { + } + + public void setHomePos(BlockPos pos) { +- this.entityData.set(Turtle.HOME_POS, pos); ++ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } + + public BlockPos getHomePos() { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 0ef67317a922f28202c84c7a1f81f3c534d2b838..81f28afeea34d5e4ebb7f8fa7e3f5f124069a5ab 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -1500,10 +1500,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public abstract RecipeManager getRecipeManager(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int l) { ++ // Paper start - allow use of mutable pos ++ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); ++ this.getRandomBlockPosition(x, y, z, l, ret); ++ return ret.immutable(); ++ } ++ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { ++ // Paper end + this.randValue = this.randValue * 3 + 1013904223; + int i1 = this.randValue >> 2; + +- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); ++ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call ++ return out; // Paper + } + + public boolean noSave() { +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 f2e11bff414b521295bde721e01ae2166a6a3fd6..8cd6c1d838e0332125fde3fc36475034aa4effa0 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -25,6 +25,7 @@ public class LevelChunkSection { + public final PalettedContainer states; + // CraftBukkit start - read/write + private PalettedContainer> biomes; ++ public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper + + public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { + // CraftBukkit end +@@ -77,6 +78,9 @@ public class LevelChunkSection { + --this.nonEmptyBlockCount; + if (iblockdata1.isRandomlyTicking()) { + --this.tickingBlockCount; ++ // Paper start ++ this.tickingList.remove(x, y, z); ++ // Paper end + } + } + +@@ -88,6 +92,9 @@ public class LevelChunkSection { + ++this.nonEmptyBlockCount; + if (state.isRandomlyTicking()) { + ++this.tickingBlockCount; ++ // Paper start ++ this.tickingList.add(x, y, z, state); ++ // Paper end + } + } + +@@ -115,40 +122,34 @@ public class LevelChunkSection { + } + + public void recalcBlockCounts() { +- class a implements PalettedContainer.CountConsumer { +- +- public int nonEmptyBlockCount; +- public int tickingBlockCount; +- public int tickingFluidCount; +- +- a(final LevelChunkSection chunksection) {} +- +- public void accept(BlockState iblockdata, int i) { ++ // Paper start - unfuck this ++ this.tickingList.clear(); ++ this.nonEmptyBlockCount = 0; ++ this.tickingBlockCount = 0; ++ this.tickingFluidCount = 0; ++ // Don't run this on clearly empty sections ++ if (this.maybeHas((BlockState state) -> !state.isAir() || !state.getFluidState().isEmpty())) { ++ this.states.forEachLocation((BlockState iblockdata, int i) -> { + FluidState fluid = iblockdata.getFluidState(); + + if (!iblockdata.isAir()) { +- this.nonEmptyBlockCount += i; ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); + if (iblockdata.isRandomlyTicking()) { +- this.tickingBlockCount += i; ++ this.tickingBlockCount = (short)(this.tickingBlockCount + 1); ++ this.tickingList.add(i, iblockdata); + } + } + + if (!fluid.isEmpty()) { +- this.nonEmptyBlockCount += i; ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); + if (fluid.isRandomlyTicking()) { +- this.tickingFluidCount += i; ++ this.tickingFluidCount = (short) (this.tickingFluidCount + 1); + } + } + +- } ++ }); + } +- +- a a0 = new a(this); +- +- this.states.count(a0); +- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; +- this.tickingBlockCount = (short) a0.tickingBlockCount; +- this.tickingFluidCount = (short) a0.tickingFluidCount; ++ // Paper end - unfuck this + } + + public PalettedContainer getStates() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 926c81a25180d508d662eb3fa35f771636164694..81368bf186365878db2e1ed305bb7bf36c26f61f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -381,6 +381,14 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + } + } + ++ // Paper start ++ public void forEachLocation(PalettedContainer.CountConsumer consumer) { ++ this.data.storage.forEach((int location, int data) -> { ++ consumer.accept(this.data.palette.valueFor(data), location); ++ }); ++ } ++ // Paper end ++ + @FunctionalInterface + public interface CountConsumer { + void accept(T object, int count); diff --git a/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch b/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch deleted file mode 100644 index aba1d9b341..0000000000 --- a/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch +++ /dev/null @@ -1,300 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Tue, 14 May 2024 11:57:43 -0700 -Subject: [PATCH] Proxy ItemStack to CraftItemStack - - -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -index be36336a3c7d1ae88277f4ee1be70075001de7a7..814e8ece6821e359b504e8c4d140cc38700f2abe 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -@@ -25,15 +25,57 @@ import org.bukkit.material.MaterialData; - @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; -+ // Paper start - delegate api-ItemStack to CraftItemStack -+ private static final java.lang.invoke.VarHandle API_ITEM_STACK_CRAFT_DELEGATE_FIELD; -+ static { -+ try { -+ API_ITEM_STACK_CRAFT_DELEGATE_FIELD = java.lang.invoke.MethodHandles.privateLookupIn( -+ ItemStack.class, -+ java.lang.invoke.MethodHandles.lookup() -+ ).findVarHandle(ItemStack.class, "craftDelegate", ItemStack.class); -+ } catch (final IllegalAccessException | NoSuchFieldException exception) { -+ throw new RuntimeException(exception); -+ } -+ } -+ -+ private static CraftItemStack getCraftStack(final ItemStack bukkit) { -+ if (bukkit instanceof final CraftItemStack craftItemStack) { -+ return craftItemStack; - } else { -- return asNMSCopy(bukkit); -+ return (CraftItemStack) API_ITEM_STACK_CRAFT_DELEGATE_FIELD.get(bukkit); - } - } - -+ @Override -+ public int hashCode() { -+ if (this.handle == null || this.handle.isEmpty()) { -+ return net.minecraft.world.item.ItemStack.EMPTY.hashCode(); -+ } else { -+ int hash = net.minecraft.world.item.ItemStack.hashItemAndComponents(this.handle); -+ hash = hash * 31 + this.handle.getCount(); -+ return hash; -+ } -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (!(obj instanceof final org.bukkit.inventory.ItemStack bukkit)) return false; -+ final CraftItemStack craftStack = getCraftStack(bukkit); -+ if (this.handle == craftStack.handle) return true; -+ else if (this.handle == null || craftStack.handle == null) return false; -+ else if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true; -+ else return net.minecraft.world.item.ItemStack.matches(this.handle, craftStack.handle); -+ } -+ // Paper end -+ -+ // Paper start - MC Utils -+ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) { -+ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack -+ final CraftItemStack craftItemStack = getCraftStack(bukkit); -+ return craftItemStack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : craftItemStack.handle; -+ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack -+ } -+ - public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) { - return old == newInstance ? unwrap(old) : asNMSCopy(newInstance); - } -@@ -47,25 +89,13 @@ public final class CraftItemStack extends ItemStack { - // Paper end - override isEmpty to use vanilla's impl - - public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) { -- if (original instanceof CraftItemStack) { -- CraftItemStack stack = (CraftItemStack) original; -- return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy(); -- } -- if (original == null || original.isEmpty()) { // Paper - override isEmpty to use vanilla's impl; use isEmpty -+ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack -+ if (original == null || original.isEmpty()) { - return net.minecraft.world.item.ItemStack.EMPTY; - } -- -- Item item = CraftItemType.bukkitToMinecraft(original.getType()); -- -- if (item == null) { -- return net.minecraft.world.item.ItemStack.EMPTY; -- } -- -- net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(item, original.getAmount()); -- if (original.hasItemMeta()) { -- CraftItemStack.setItemMeta(stack, original.getItemMeta()); -- } -- return stack; -+ final CraftItemStack stack = getCraftStack(original); -+ return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy(); -+ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack - } - - // Paper start -@@ -88,14 +118,10 @@ public final class CraftItemStack extends ItemStack { - * Copies the NMS stack to return as a strictly-Bukkit stack - */ - public static ItemStack asBukkitCopy(net.minecraft.world.item.ItemStack original) { -- if (original.isEmpty()) { -- return new ItemStack(Material.AIR); -- } -- ItemStack stack = new ItemStack(CraftItemType.minecraftToBukkit(original.getItem()), original.getCount()); -- if (CraftItemStack.hasItemMeta(original)) { -- stack.setItemMeta(CraftItemStack.getItemMeta(original)); -- } -- return stack; -+ // Paper start - no such thing as a "strictly-Bukkit stack" anymore -+ // we copy the stack since it should be a complete copy not a mirror -+ return asCraftMirror(original.copy()); -+ // Paper end - } - - public static CraftItemStack asCraftMirror(net.minecraft.world.item.ItemStack original) { -@@ -313,11 +339,7 @@ public final class CraftItemStack extends ItemStack { - - @Override - public CraftItemStack clone() { -- CraftItemStack itemStack = (CraftItemStack) super.clone(); -- if (this.handle != null) { -- itemStack.handle = this.handle.copy(); -- } -- return itemStack; -+ return new org.bukkit.craftbukkit.inventory.CraftItemStack(this.handle != null ? this.handle.copy() : null); // Paper - } - - @Override -@@ -420,22 +442,14 @@ public final class CraftItemStack extends ItemStack { - if (stack == this) { - return true; - } -- if (!(stack instanceof CraftItemStack)) { -- return stack.getClass() == ItemStack.class && stack.isSimilar(this); -- } -- -- CraftItemStack that = (CraftItemStack) stack; -+ final CraftItemStack that = getCraftStack(stack); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack - if (this.handle == that.handle) { - return true; - } - if (this.handle == null || that.handle == null) { - return false; - } -- Material comparisonType = CraftLegacy.fromLegacy(that.getType()); // This may be called from legacy item stacks, try to get the right material -- if (!(comparisonType == this.getType() && this.getDurability() == that.getDurability())) { -- return false; -- } -- return this.hasItemMeta() ? that.hasItemMeta() && this.handle.getComponents().equals(that.handle.getComponents()) : !that.hasItemMeta(); -+ return net.minecraft.world.item.ItemStack.isSameItemSameComponents(this.handle, that.handle); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java -index 9d2feff3df87cfeefe7105d954854af05cef6f69..07539ebfefa3352de5ee7a17f2724cf2c979f399 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java -@@ -100,13 +100,14 @@ public class CraftItemType implements ItemType.Typed, Han - @NotNull - @Override - public ItemStack createItemStack(final int amount, @Nullable final Consumer metaConfigurator) { -- final ItemStack itemStack = new ItemStack(this.asMaterial(), amount); -+ // Paper start - re-implement to return CraftItemStack -+ final net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(this.item, amount); -+ final CraftItemStack mirror = CraftItemStack.asCraftMirror(stack); - if (metaConfigurator != null) { -- final ItemMeta itemMeta = itemStack.getItemMeta(); -- metaConfigurator.accept((M) itemMeta); -- itemStack.setItemMeta(itemMeta); -+ mirror.editMeta(this.getItemMetaClass(), metaConfigurator); - } -- return itemStack; -+ return mirror; -+ // Paper start - reimplement to return CraftItemStack - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java -index 9c004e7cb46841d874ab997bf2e3b63ae763aec7..d7c8f26b21276d9ff1d5c7c9738cc1126ce7d4b9 100644 ---- a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java -+++ b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java -@@ -678,4 +678,16 @@ public class MaterialRerouting { - return itemStack.withType(material); - } - // Paper end - register paper API specific material consumers in rerouting -+ -+ // Paper start - methods added post 1.13, no-op -+ @RerouteStatic("org/bukkit/inventory/ItemStack") -+ public static ItemStack of(final Material material) { -+ return ItemStack.of(material); -+ } -+ -+ @RerouteStatic("org/bukkit/inventory/ItemStack") -+ public static ItemStack of(final Material material, final int amount) { -+ return ItemStack.of(material, amount); -+ } -+ // Paper end - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae..d70c5546c8bd6f364fad9b24880b6867efdab644 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -700,6 +700,13 @@ public final class CraftMagicNumbers implements UnsafeValues { - } - // Paper end - hack to get tags for non server-backed registries - -+ // Paper start - proxy ItemStack -+ @Override -+ public org.bukkit.inventory.ItemStack createEmptyStack() { -+ return CraftItemStack.asCraftMirror(null); -+ } -+ // Paper end - proxy ItemStack -+ - /** - * This helper class represents the different NBT Tags. - *

-diff --git a/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e5c2fb160e9d390cdfa0259a3feb9f488b2dc14d ---- /dev/null -+++ b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java -@@ -0,0 +1,53 @@ -+package io.papermc.paper.configuration; -+ -+import org.bukkit.Material; -+import org.bukkit.configuration.ConfigurationSection; -+import org.bukkit.inventory.ItemStack; -+import org.bukkit.support.AbstractTestingBase; -+import org.junit.jupiter.api.Test; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertFalse; -+import static org.junit.jupiter.api.Assertions.assertNull; -+import static org.junit.jupiter.api.Assertions.assertTrue; -+ -+public abstract class ConfigurationSectionTest extends AbstractTestingBase { -+ public abstract ConfigurationSection getConfigurationSection(); -+ -+ @Test -+ public void testGetItemStack_String() { -+ ConfigurationSection section = getConfigurationSection(); -+ String key = "exists"; -+ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); -+ -+ section.set(key, value); -+ -+ assertEquals(value, section.getItemStack(key)); -+ assertNull(section.getString("doesntExist")); -+ } -+ -+ @Test -+ public void testGetItemStack_String_ItemStack() { -+ ConfigurationSection section = getConfigurationSection(); -+ String key = "exists"; -+ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); -+ ItemStack def = new ItemStack(Material.STONE, 1); -+ -+ section.set(key, value); -+ -+ assertEquals(value, section.getItemStack(key, def)); -+ assertEquals(def, section.getItemStack("doesntExist", def)); -+ } -+ -+ @Test -+ public void testIsItemStack() { -+ ConfigurationSection section = getConfigurationSection(); -+ String key = "exists"; -+ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); -+ -+ section.set(key, value); -+ -+ assertTrue(section.isItemStack(key)); -+ assertFalse(section.isItemStack("doesntExist")); -+ } -+} -diff --git a/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..def33c36f207a4c5306b5a895336aa70335c1678 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java -@@ -0,0 +1,11 @@ -+package io.papermc.paper.configuration; -+ -+import org.bukkit.configuration.ConfigurationSection; -+import org.bukkit.configuration.MemoryConfiguration; -+ -+public class MemorySectionTest extends ConfigurationSectionTest { -+ @Override -+ public ConfigurationSection getConfigurationSection() { -+ return new MemoryConfiguration().createSection("section"); -+ } -+} diff --git a/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch deleted file mode 100644 index 95338b76bf..0000000000 --- a/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch +++ /dev/null @@ -1,266 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Wed, 12 Jun 2024 10:29:40 -0700 -Subject: [PATCH] Make a PDC view accessible directly from ItemStack - - -diff --git a/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fac401280d3f3689b00e16c19155ca753faa06e0 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java -@@ -0,0 +1,86 @@ -+package io.papermc.paper.persistence; -+ -+import com.google.common.base.Preconditions; -+import java.io.ByteArrayOutputStream; -+import java.io.DataOutputStream; -+import java.io.IOException; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.NbtIo; -+import net.minecraft.nbt.Tag; -+import org.bukkit.NamespacedKey; -+import org.bukkit.craftbukkit.persistence.CraftPersistentDataAdapterContext; -+import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry; -+import org.bukkit.persistence.PersistentDataAdapterContext; -+import org.bukkit.persistence.PersistentDataType; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public abstract class PaperPersistentDataContainerView implements PersistentDataContainerView { -+ -+ protected final CraftPersistentDataTypeRegistry registry; -+ protected final CraftPersistentDataAdapterContext adapterContext; -+ -+ public PaperPersistentDataContainerView(final CraftPersistentDataTypeRegistry registry) { -+ this.registry = registry; -+ this.adapterContext = new CraftPersistentDataAdapterContext(this.registry); -+ } -+ -+ public abstract @Nullable Tag getTag(final String key); -+ -+ public abstract CompoundTag toTagCompound(); -+ -+ @Override -+ public boolean has(final NamespacedKey key, final PersistentDataType type) { -+ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); -+ Preconditions.checkArgument(type != null, "The provided type cannot be null"); -+ -+ final @Nullable Tag value = this.getTag(key.toString()); -+ if (value == null) { -+ return false; -+ } -+ -+ return this.registry.isInstanceOf(type, value); -+ } -+ -+ @Override -+ public boolean has(final NamespacedKey key) { -+ Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper -+ return this.getTag(key.toString()) != null; -+ } -+ -+ @Override -+ public @Nullable C get(final NamespacedKey key, final PersistentDataType type) { -+ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); -+ Preconditions.checkArgument(type != null, "The provided type cannot be null"); -+ -+ final @Nullable Tag value = this.getTag(key.toString()); -+ if (value == null) { -+ return null; -+ } -+ -+ return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext); -+ } -+ -+ @Override -+ public C getOrDefault(final NamespacedKey key, final PersistentDataType type, final C defaultValue) { -+ final C c = this.get(key, type); -+ return c != null ? c : defaultValue; -+ } -+ -+ @Override -+ public PersistentDataAdapterContext getAdapterContext() { -+ return this.adapterContext; -+ } -+ -+ @Override -+ public byte[] serializeToBytes() throws IOException { -+ final net.minecraft.nbt.CompoundTag root = this.toTagCompound(); -+ final ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); -+ try (final DataOutputStream dataOutput = new DataOutputStream(byteArrayOutput)) { -+ NbtIo.write(root, dataOutput); -+ return byteArrayOutput.toByteArray(); -+ } -+ } -+} -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -index 814e8ece6821e359b504e8c4d140cc38700f2abe..32a41c8b324aad67b9dcf74387aef299e6478a64 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -@@ -480,4 +480,63 @@ public final class CraftItemStack extends ItemStack { - return mirrored; - } - // Paper end -+ -+ // Paper start - pdc -+ private net.minecraft.nbt.CompoundTag getPdcTag() { -+ if (this.handle == null) { -+ return new net.minecraft.nbt.CompoundTag(); -+ } -+ final net.minecraft.world.item.component.CustomData customData = this.handle.getOrDefault(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY); -+ // getUnsafe is OK here because we are only ever *reading* the data so immutability is preserved -+ //noinspection deprecation -+ return customData.getUnsafe().getCompound("PublicBukkitValues"); -+ } -+ -+ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); -+ private final io.papermc.paper.persistence.PaperPersistentDataContainerView pdcView = new io.papermc.paper.persistence.PaperPersistentDataContainerView(REGISTRY) { -+ -+ @Override -+ public net.minecraft.nbt.CompoundTag toTagCompound() { -+ return CraftItemStack.this.getPdcTag(); -+ } -+ -+ @Override -+ public net.minecraft.nbt.Tag getTag(final String key) { -+ return CraftItemStack.this.getPdcTag().get(key); -+ } -+ -+ @Override -+ public java.util.Set getKeys() { -+ java.util.Set keys = new java.util.HashSet<>(); -+ CraftItemStack.this.getPdcTag().getAllKeys().forEach(key -> { -+ final String[] keyData = key.split(":", 2); -+ if (keyData.length == 2) { -+ keys.add(new org.bukkit.NamespacedKey(keyData[0], keyData[1])); -+ } -+ }); -+ return java.util.Collections.unmodifiableSet(keys); -+ }; -+ -+ @Override -+ public boolean isEmpty() { -+ return CraftItemStack.this.getPdcTag().isEmpty(); -+ } -+ -+ @Override -+ public void copyTo(final org.bukkit.persistence.PersistentDataContainer other, final boolean replace) { -+ Preconditions.checkArgument(other != null, "The target container cannot be null"); -+ final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer target = (org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer) other; -+ final net.minecraft.nbt.CompoundTag pdcTag = org.bukkit.craftbukkit.inventory.CraftItemStack.this.getPdcTag(); -+ for (final String key : pdcTag.getAllKeys()) { -+ if (replace || !target.getRaw().containsKey(key)) { -+ target.getRaw().put(key, pdcTag.get(key).copy()); -+ } -+ } -+ } -+ }; -+ @Override -+ public io.papermc.paper.persistence.PersistentDataContainerView getPersistentDataContainer() { -+ return this.pdcView; -+ } -+ // Paper end - pdc - } -diff --git a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java -index f55fdd57ced259ad5a95878840e98ffaa3db2e05..9d867f1659433ea15f281c8b441db7e339013100 100644 ---- a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java -+++ b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java -@@ -16,11 +16,10 @@ import org.bukkit.persistence.PersistentDataContainer; - import org.bukkit.persistence.PersistentDataType; - import org.jetbrains.annotations.NotNull; - --public class CraftPersistentDataContainer implements PersistentDataContainer { -+public class CraftPersistentDataContainer extends io.papermc.paper.persistence.PaperPersistentDataContainerView implements PersistentDataContainer { // Paper - split up view and mutable - - private final Map customDataTags = new HashMap<>(); -- private final CraftPersistentDataTypeRegistry registry; -- private final CraftPersistentDataAdapterContext adapterContext; -+ // Paper - move to PersistentDataContainerView - - public CraftPersistentDataContainer(Map customTags, CraftPersistentDataTypeRegistry registry) { - this(registry); -@@ -28,10 +27,15 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { - } - - public CraftPersistentDataContainer(CraftPersistentDataTypeRegistry registry) { -- this.registry = registry; -- this.adapterContext = new CraftPersistentDataAdapterContext(this.registry); -+ super(registry); // Paper - move to PersistentDataContainerView - } - -+ // Paper start -+ @Override -+ public Tag getTag(final String key) { -+ return this.customDataTags.get(key); -+ } -+ // Paper end - - @Override - public void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z value) { -@@ -42,44 +46,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { - this.customDataTags.put(key.toString(), this.registry.wrap(type, type.toPrimitive(value, this.adapterContext))); - } - -- @Override -- public boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { -- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); -- Preconditions.checkArgument(type != null, "The provided type cannot be null"); -- -- Tag value = this.customDataTags.get(key.toString()); -- if (value == null) { -- return false; -- } -- -- return this.registry.isInstanceOf(type, value); -- } -- -- @Override -- public boolean has(NamespacedKey key) { -- Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper -- return this.customDataTags.get(key.toString()) != null; -- } -- -- @Override -- public Z get(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { -- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); -- Preconditions.checkArgument(type != null, "The provided type cannot be null"); -- -- Tag value = this.customDataTags.get(key.toString()); -- if (value == null) { -- return null; -- } -- -- return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext); -- } -- -- @NotNull -- @Override -- public Z getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z defaultValue) { -- Z z = this.get(key, type); -- return z != null ? z : defaultValue; -- } -+ // Paper - move to PersistentDataContainerView - - @NotNull - @Override -@@ -186,16 +153,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { - // Paper end - - // Paper start - byte array serialization -- @Override -- public byte[] serializeToBytes() throws java.io.IOException { -- final net.minecraft.nbt.CompoundTag root = this.toTagCompound(); -- final java.io.ByteArrayOutputStream byteArrayOutput = new java.io.ByteArrayOutputStream(); -- try (final java.io.DataOutputStream dataOutput = new java.io.DataOutputStream(byteArrayOutput)) { -- net.minecraft.nbt.NbtIo.write(root, dataOutput); -- return byteArrayOutput.toByteArray(); -- } -- } -- -+ // Paper - move to PersistentDataContainerView - @Override - public void readFromBytes(final byte[] bytes, final boolean clear) throws java.io.IOException { - if (clear) { diff --git a/patches/server/1025-Registry-Modification-API.patch b/patches/server/1025-Registry-Modification-API.patch new file mode 100644 index 0000000000..c45b119038 --- /dev/null +++ b/patches/server/1025-Registry-Modification-API.patch @@ -0,0 +1,1421 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Mon, 27 Feb 2023 18:28:39 -0800 +Subject: [PATCH] Registry Modification API + +== AT == +public net.minecraft.core.MappedRegistry validateWrite(Lnet/minecraft/resources/ResourceKey;)V +public net.minecraft.resources.RegistryOps lookupProvider +public net.minecraft.resources.RegistryOps$HolderLookupAdapter + +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +index 1e098dc25bd338ff179491ff3382ac56aad9948e..a688af29273ebfbb4f75dd74cd30627fc481c96c 100644 +--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +@@ -2,6 +2,7 @@ package io.papermc.paper.registry; + + import io.papermc.paper.adventure.PaperAdventure; + import io.papermc.paper.registry.entry.RegistryEntry; ++import io.papermc.paper.registry.tag.TagKey; + import java.util.Collections; + import java.util.IdentityHashMap; + import java.util.List; +@@ -46,6 +47,7 @@ import org.checkerframework.framework.qual.DefaultQualifier; + + import static io.papermc.paper.registry.entry.RegistryEntry.apiOnly; + import static io.papermc.paper.registry.entry.RegistryEntry.entry; ++import static io.papermc.paper.registry.entry.RegistryEntry.writable; + + @DefaultQualifier(NonNull.class) + public final class PaperRegistries { +@@ -128,6 +130,15 @@ public final class PaperRegistries { + return ResourceKey.create((ResourceKey>) PaperRegistries.registryToNms(typedKey.registryKey()), PaperAdventure.asVanilla(typedKey.key())); + } + ++ public static TagKey fromNms(final net.minecraft.tags.TagKey tagKey) { ++ return TagKey.create(registryFromNms(tagKey.registry()), CraftNamespacedKey.fromMinecraft(tagKey.location())); ++ } ++ ++ @SuppressWarnings({"unchecked", "RedundantCast"}) ++ public static net.minecraft.tags.TagKey toNms(final TagKey tagKey) { ++ return net.minecraft.tags.TagKey.create((ResourceKey>) registryToNms(tagKey.registryKey()), PaperAdventure.asVanilla(tagKey.key())); ++ } ++ + private PaperRegistries() { + } + } +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java +index d591e3a2e19d5358a0d25a5a681368943622d231..f05ebf453406a924da3de6fb250f4793a1b3c612 100644 +--- a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java +@@ -80,6 +80,14 @@ public class PaperRegistryAccess implements RegistryAccess { + return possiblyUnwrap(registryHolder.get()); + } + ++ public > WritableCraftRegistry getWritableRegistry(final RegistryKey key) { ++ final Registry registry = this.getRegistry(key); ++ if (registry instanceof WritableCraftRegistry) { ++ return (WritableCraftRegistry) registry; ++ } ++ throw new IllegalArgumentException(key + " does not point to a writable registry"); ++ } ++ + private static Registry possiblyUnwrap(final Registry registry) { + if (registry instanceof final DelayedRegistry delayedRegistry) { // if not coming from legacy, unwrap the delayed registry + return delayedRegistry.delegate(); +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..528c6ee1739d92f766f3904acd7fc5734c93388a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.registry; ++ ++import io.papermc.paper.registry.data.util.Conversions; ++import net.minecraft.resources.RegistryOps; ++import org.checkerframework.checker.nullness.qual.Nullable; ++ ++public interface PaperRegistryBuilder extends RegistryBuilder { ++ ++ M build(); ++ ++ @FunctionalInterface ++ interface Filler> { ++ ++ B fill(Conversions conversions, TypedKey key, @Nullable M nms); ++ ++ default Factory asFactory() { ++ return (lookup, key) -> this.fill(lookup, key, null); ++ } ++ } ++ ++ @FunctionalInterface ++ interface Factory> { ++ ++ B create(Conversions conversions, TypedKey key); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7f2b264d4f37f5293ae72195b4c78faf35351c9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java +@@ -0,0 +1,183 @@ ++package io.papermc.paper.registry; ++ ++import com.google.common.base.Preconditions; ++import com.mojang.serialization.Lifecycle; ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.entrypoint.Entrypoint; ++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; ++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.entry.RegistryEntry; ++import io.papermc.paper.registry.entry.RegistryEntryInfo; ++import io.papermc.paper.registry.event.RegistryEntryAddEventImpl; ++import io.papermc.paper.registry.event.RegistryEventMap; ++import io.papermc.paper.registry.event.RegistryEventProvider; ++import io.papermc.paper.registry.event.RegistryFreezeEvent; ++import io.papermc.paper.registry.event.RegistryFreezeEventImpl; ++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType; ++import io.papermc.paper.registry.event.type.RegistryEntryAddEventTypeImpl; ++import io.papermc.paper.registry.event.type.RegistryLifecycleEventType; ++import java.util.Optional; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.Holder; ++import net.minecraft.core.MappedRegistry; ++import net.minecraft.core.RegistrationInfo; ++import net.minecraft.core.Registry; ++import net.minecraft.core.WritableRegistry; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.intellij.lang.annotations.Subst; ++ ++public final class PaperRegistryListenerManager { ++ ++ public static final PaperRegistryListenerManager INSTANCE = new PaperRegistryListenerManager(); ++ ++ public final RegistryEventMap valueAddHooks = new RegistryEventMap("value add"); ++ public final RegistryEventMap freezeHooks = new RegistryEventMap("freeze"); ++ ++ private PaperRegistryListenerManager() { ++ } ++ ++ /** ++ * For {@link Registry#register(Registry, String, Object)} ++ */ ++ public M registerWithListeners(final Registry registry, final String id, final M nms) { ++ return this.registerWithListeners(registry, ResourceLocation.withDefaultNamespace(id), nms); ++ } ++ ++ /** ++ * For {@link Registry#register(Registry, ResourceLocation, Object)} ++ */ ++ public M registerWithListeners(final Registry registry, final ResourceLocation loc, final M nms) { ++ return this.registerWithListeners(registry, ResourceKey.create(registry.key(), loc), nms); ++ } ++ ++ /** ++ * For {@link Registry#register(Registry, ResourceKey, Object)} ++ */ ++ public M registerWithListeners(final Registry registry, final ResourceKey key, final M nms) { ++ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, PaperRegistryListenerManager::registerWithInstance, BuiltInRegistries.BUILT_IN_CONVERSIONS); ++ } ++ ++ /** ++ * For {@link Registry#registerForHolder(Registry, ResourceLocation, Object)} ++ */ ++ public Holder.Reference registerForHolderWithListeners(final Registry registry, final ResourceLocation loc, final M nms) { ++ return this.registerForHolderWithListeners(registry, ResourceKey.create(registry.key(), loc), nms); ++ } ++ ++ /** ++ * For {@link Registry#registerForHolder(Registry, ResourceKey, Object)} ++ */ ++ public Holder.Reference registerForHolderWithListeners(final Registry registry, final ResourceKey key, final M nms) { ++ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, WritableRegistry::register, BuiltInRegistries.BUILT_IN_CONVERSIONS); ++ } ++ ++ public void registerWithListeners( ++ final Registry registry, ++ final ResourceKey key, ++ final M nms, ++ final RegistrationInfo registrationInfo, ++ final Conversions conversions ++ ) { ++ this.registerWithListeners(registry, key, nms, registrationInfo, WritableRegistry::register, conversions); ++ } ++ ++ // TODO remove Keyed ++ public , R> R registerWithListeners( ++ final Registry registry, ++ final ResourceKey key, ++ final M nms, ++ final RegistrationInfo registrationInfo, ++ final RegisterMethod registerMethod, ++ final Conversions conversions ++ ) { ++ Preconditions.checkState(LaunchEntryPointHandler.INSTANCE.hasEntered(Entrypoint.BOOTSTRAPPER), registry.key() + " tried to run modification listeners before bootstrappers have been called"); // verify that bootstrappers have been called ++ final @Nullable RegistryEntryInfo entry = PaperRegistries.getEntry(registry.key()); ++ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddHooks.hasHooks(entry.apiKey())) { ++ return registerMethod.register((WritableRegistry) registry, key, nms, registrationInfo); ++ } ++ final RegistryEntry.Modifiable modifiableEntry = RegistryEntry.Modifiable.asModifiable(entry); ++ @SuppressWarnings("PatternValidation") final TypedKey typedKey = TypedKey.create(entry.apiKey(), Key.key(key.location().getNamespace(), key.location().getPath())); ++ final B builder = modifiableEntry.fillBuilder(conversions, typedKey, nms); ++ return this.registerWithListeners(registry, modifiableEntry, key, nms, builder, registrationInfo, registerMethod, conversions); ++ } ++ ++ > void registerWithListeners( // TODO remove Keyed ++ final WritableRegistry registry, ++ final RegistryEntryInfo entry, ++ final ResourceKey key, ++ final B builder, ++ final RegistrationInfo registrationInfo, ++ final Conversions conversions ++ ) { ++ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddHooks.hasHooks(entry.apiKey())) { ++ registry.register(key, builder.build(), registrationInfo); ++ return; ++ } ++ this.registerWithListeners(registry, RegistryEntry.Modifiable.asModifiable(entry), key, null, builder, registrationInfo, WritableRegistry::register, conversions); ++ } ++ ++ public , R> R registerWithListeners( // TODO remove Keyed ++ final Registry registry, ++ final RegistryEntry.Modifiable entry, ++ final ResourceKey key, ++ final @Nullable M oldNms, ++ final B builder, ++ RegistrationInfo registrationInfo, ++ final RegisterMethod registerMethod, ++ final Conversions conversions ++ ) { ++ @Subst("namespace:key") final ResourceLocation beingAdded = key.location(); ++ @SuppressWarnings("PatternValidation") final TypedKey typedKey = TypedKey.create(entry.apiKey(), Key.key(beingAdded.getNamespace(), beingAdded.getPath())); ++ final RegistryEntryAddEventImpl event = entry.createEntryAddEvent(typedKey, builder, conversions); ++ LifecycleEventRunner.INSTANCE.callEvent(this.valueAddHooks.getHook(entry.apiKey()), event); ++ if (oldNms != null) { ++ ((MappedRegistry) registry).clearIntrusiveHolder(oldNms); ++ } ++ final M newNms = event.builder().build(); ++ if (oldNms != null && !newNms.equals(oldNms)) { ++ registrationInfo = new RegistrationInfo(Optional.empty(), Lifecycle.experimental()); ++ } ++ return registerMethod.register((WritableRegistry) registry, key, newNms, registrationInfo); ++ } ++ ++ private static M registerWithInstance(final WritableRegistry writableRegistry, final ResourceKey key, final M value, final RegistrationInfo registrationInfo) { ++ writableRegistry.register(key, value, registrationInfo); ++ return value; ++ } ++ ++ @FunctionalInterface ++ public interface RegisterMethod { ++ ++ R register(WritableRegistry writableRegistry, ResourceKey key, M value, RegistrationInfo registrationInfo); ++ } ++ ++ public > void runFreezeListeners(final ResourceKey> resourceKey, final Conversions conversions) { ++ final @Nullable RegistryEntryInfo entry = PaperRegistries.getEntry(resourceKey); ++ if (!RegistryEntry.Addable.isAddable(entry) || !this.freezeHooks.hasHooks(entry.apiKey())) { ++ return; ++ } ++ final RegistryEntry.Addable writableEntry = RegistryEntry.Addable.asAddable(entry); ++ final WritableCraftRegistry writableRegistry = PaperRegistryAccess.instance().getWritableRegistry(entry.apiKey()); ++ final RegistryFreezeEventImpl event = writableEntry.createFreezeEvent(writableRegistry, conversions); ++ LifecycleEventRunner.INSTANCE.callEvent(this.freezeHooks.getHook(entry.apiKey()), event); ++ } ++ ++ public > RegistryEntryAddEventType getRegistryValueAddEventType(final RegistryEventProvider type) { ++ if (!RegistryEntry.Modifiable.isModifiable(PaperRegistries.getEntry(type.registryKey()))) { ++ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryEntryAddEvent"); ++ } ++ return this.valueAddHooks.getOrCreate(type, RegistryEntryAddEventTypeImpl::new); ++ } ++ ++ public > LifecycleEventType.Prioritizable> getRegistryFreezeEventType(final RegistryEventProvider type) { ++ if (!RegistryEntry.Addable.isAddable(PaperRegistries.getEntry(type.registryKey()))) { ++ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryFreezeEvent"); ++ } ++ return this.freezeHooks.getOrCreate(type, RegistryLifecycleEventType::new); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..78317c7ab42a666f19634593a8f3b696700764c8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java +@@ -0,0 +1,92 @@ ++package io.papermc.paper.registry; ++ ++import com.mojang.serialization.Lifecycle; ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.entry.RegistryEntry; ++import io.papermc.paper.registry.event.WritableRegistry; ++import java.util.Optional; ++import java.util.function.BiFunction; ++import java.util.function.Consumer; ++import net.minecraft.core.MappedRegistry; ++import net.minecraft.core.RegistrationInfo; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.bukkit.craftbukkit.util.ApiVersion; ++import org.checkerframework.checker.nullness.qual.Nullable; ++ ++public class WritableCraftRegistry> extends CraftRegistry { ++ ++ private static final RegistrationInfo FROM_PLUGIN = new RegistrationInfo(Optional.empty(), Lifecycle.experimental()); ++ ++ private final RegistryEntry.BuilderHolder entry; ++ private final MappedRegistry registry; ++ private final PaperRegistryBuilder.Factory builderFactory; ++ private final BiFunction minecraftToBukkit; ++ ++ public WritableCraftRegistry( ++ final RegistryEntry.BuilderHolder entry, ++ final Class classToPreload, ++ final MappedRegistry registry, ++ final BiFunction serializationUpdater, ++ final PaperRegistryBuilder.Factory builderFactory, ++ final BiFunction minecraftToBukkit ++ ) { ++ super(classToPreload, registry, null, serializationUpdater); ++ this.entry = entry; ++ this.registry = registry; ++ this.builderFactory = builderFactory; ++ this.minecraftToBukkit = minecraftToBukkit; ++ } ++ ++ public void register(final TypedKey key, final Consumer value, final Conversions conversions) { ++ final ResourceKey resourceKey = ResourceKey.create(this.registry.key(), PaperAdventure.asVanilla(key.key())); ++ this.registry.validateWrite(resourceKey); ++ final B builder = this.newBuilder(conversions, key); ++ value.accept(builder); ++ PaperRegistryListenerManager.INSTANCE.registerWithListeners( ++ this.registry, ++ RegistryEntry.Modifiable.asModifiable(this.entry), ++ resourceKey, ++ builder, ++ FROM_PLUGIN, ++ conversions ++ ); ++ } ++ ++ @Override ++ public final @Nullable T createBukkit(final NamespacedKey namespacedKey, final @Nullable M minecraft) { ++ if (minecraft == null) { ++ return null; ++ } ++ return this.minecraftToBukkit(namespacedKey, minecraft); ++ } ++ ++ public WritableRegistry createApiWritableRegistry(final Conversions conversions) { ++ return new ApiWritableRegistry(conversions); ++ } ++ ++ public T minecraftToBukkit(final NamespacedKey namespacedKey, final M minecraft) { ++ return this.minecraftToBukkit.apply(namespacedKey, minecraft); ++ } ++ ++ protected B newBuilder(final Conversions conversions, final TypedKey key) { ++ return this.builderFactory.create(conversions, key); ++ } ++ ++ public class ApiWritableRegistry implements WritableRegistry { ++ ++ private final Conversions conversions; ++ ++ public ApiWritableRegistry(final Conversions conversions) { ++ this.conversions = conversions; ++ } ++ ++ @Override ++ public void register(final TypedKey key, final Consumer value) { ++ WritableCraftRegistry.this.register(key, value, this.conversions); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/data/util/Conversions.java b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eda5cc7d45ef59ccc1c9c7e027c1f044f1dcc86b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java +@@ -0,0 +1,36 @@ ++package io.papermc.paper.registry.data.util; ++ ++import com.mojang.serialization.JavaOps; ++import io.papermc.paper.adventure.WrapperAwareSerializer; ++import net.kyori.adventure.text.Component; ++import net.minecraft.resources.RegistryOps; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.Contract; ++ ++@DefaultQualifier(NonNull.class) ++public class Conversions { ++ ++ private final RegistryOps.RegistryInfoLookup lookup; ++ private final WrapperAwareSerializer serializer; ++ ++ public Conversions(final RegistryOps.RegistryInfoLookup lookup) { ++ this.lookup = lookup; ++ this.serializer = new WrapperAwareSerializer(() -> RegistryOps.create(JavaOps.INSTANCE, lookup)); ++ } ++ ++ public RegistryOps.RegistryInfoLookup lookup() { ++ return this.lookup; ++ } ++ ++ @Contract("null -> null; !null -> !null") ++ public net.minecraft.network.chat.@Nullable Component asVanilla(final @Nullable Component adventure) { ++ if (adventure == null) return null; ++ return this.serializer.serialize(adventure); ++ } ++ ++ public Component asAdventure(final net.minecraft.network.chat.@Nullable Component vanilla) { ++ return vanilla == null ? Component.empty() : this.serializer.deserialize(vanilla); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aeec9b3ae2911f041d000b3db72f37974020ba60 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.registry.entry; ++ ++import io.papermc.paper.registry.PaperRegistryBuilder; ++import io.papermc.paper.registry.RegistryHolder; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.WritableCraftRegistry; ++import io.papermc.paper.registry.data.util.Conversions; ++import java.util.function.BiFunction; ++import net.minecraft.core.MappedRegistry; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++ ++public class AddableRegistryEntry> extends CraftRegistryEntry implements RegistryEntry.Addable { ++ ++ private final PaperRegistryBuilder.Filler builderFiller; ++ ++ protected AddableRegistryEntry( ++ final ResourceKey> mcKey, ++ final RegistryKey apiKey, ++ final Class classToPreload, ++ final BiFunction minecraftToBukkit, ++ final PaperRegistryBuilder.Filler builderFiller ++ ) { ++ super(mcKey, apiKey, classToPreload, minecraftToBukkit); ++ this.builderFiller = builderFiller; ++ } ++ ++ private WritableCraftRegistry createRegistry(final Registry registry) { ++ return new WritableCraftRegistry<>(this, this.classToPreload, (MappedRegistry) registry, this.updater, this.builderFiller.asFactory(), this.minecraftToBukkit); ++ } ++ ++ @Override ++ public RegistryHolder createRegistryHolder(final Registry nmsRegistry) { ++ return new RegistryHolder.Memoized<>(() -> this.createRegistry(nmsRegistry)); ++ } ++ ++ @Override ++ public B fillBuilder(final Conversions conversions, final TypedKey key, final M nms) { ++ return this.builderFiller.fill(conversions, key, nms); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..515a995e3862f8e7cb93d149315ea32e04a08716 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.registry.entry; ++ ++import io.papermc.paper.registry.PaperRegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.data.util.Conversions; ++import java.util.function.BiFunction; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++ ++public class ModifiableRegistryEntry> extends CraftRegistryEntry implements RegistryEntry.Modifiable { ++ ++ protected final PaperRegistryBuilder.Filler builderFiller; ++ ++ protected ModifiableRegistryEntry( ++ final ResourceKey> mcKey, ++ final RegistryKey apiKey, ++ final Class toPreload, ++ final BiFunction minecraftToBukkit, ++ final PaperRegistryBuilder.Filler builderFiller ++ ) { ++ super(mcKey, apiKey, toPreload, minecraftToBukkit); ++ this.builderFiller = builderFiller; ++ } ++ ++ @Override ++ public B fillBuilder(final Conversions conversions, final TypedKey key, final M nms) { ++ return this.builderFiller.fill(conversions, key, nms); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java +index 15991bf13894d850f360a520d1815711d25973ec..f2e919705301cb23ed1938ca3c1976378249172c 100644 +--- a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java ++++ b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java +@@ -1,7 +1,13 @@ + package io.papermc.paper.registry.entry; + ++import io.papermc.paper.registry.PaperRegistryBuilder; + import io.papermc.paper.registry.RegistryHolder; + import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.WritableCraftRegistry; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.event.RegistryEntryAddEventImpl; ++import io.papermc.paper.registry.event.RegistryFreezeEventImpl; + import io.papermc.paper.registry.legacy.DelayedRegistryEntry; + import java.util.function.BiFunction; + import java.util.function.Supplier; +@@ -11,6 +17,7 @@ import org.bukkit.Keyed; + import org.bukkit.NamespacedKey; + import org.bukkit.craftbukkit.util.ApiVersion; + import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; + import org.checkerframework.framework.qual.DefaultQualifier; + + @DefaultQualifier(NonNull.class) +@@ -32,6 +39,65 @@ public interface RegistryEntry extends RegistryEntryInfo(this); + } + ++ interface BuilderHolder> extends RegistryEntryInfo { ++ ++ B fillBuilder(Conversions conversions, TypedKey key, M nms); ++ } ++ ++ /** ++ * Can mutate values being added to the registry ++ */ ++ interface Modifiable> extends BuilderHolder { ++ ++ static boolean isModifiable(final @Nullable RegistryEntryInfo entry) { ++ return entry instanceof RegistryEntry.Modifiable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Modifiable); ++ } ++ ++ static > Modifiable asModifiable(final RegistryEntryInfo entry) { // TODO remove Keyed ++ return (Modifiable) possiblyUnwrap(entry); ++ } ++ ++ default RegistryEntryAddEventImpl createEntryAddEvent(final TypedKey key, final B initialBuilder, final Conversions conversions) { ++ return new RegistryEntryAddEventImpl<>(key, initialBuilder, this.apiKey(), conversions); ++ } ++ } ++ ++ /** ++ * Can only add new values to the registry, not modify any values. ++ */ ++ interface Addable> extends BuilderHolder { // TODO remove Keyed ++ ++ default RegistryFreezeEventImpl createFreezeEvent(final WritableCraftRegistry writableRegistry, final Conversions conversions) { ++ return new RegistryFreezeEventImpl<>(this.apiKey(), writableRegistry.createApiWritableRegistry(conversions), conversions); ++ } ++ ++ static boolean isAddable(final @Nullable RegistryEntryInfo entry) { ++ return entry instanceof RegistryEntry.Addable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Addable); ++ } ++ ++ static > Addable asAddable(final RegistryEntryInfo entry) { ++ return (Addable) possiblyUnwrap(entry); ++ } ++ } ++ ++ /** ++ * Can mutate values and add new values. ++ */ ++ interface Writable> extends Modifiable, Addable { // TODO remove Keyed ++ ++ static boolean isWritable(final @Nullable RegistryEntryInfo entry) { ++ return entry instanceof RegistryEntry.Writable || (entry instanceof final DelayedRegistryEntry delayed && delayed.delegate() instanceof RegistryEntry.Writable); ++ } ++ ++ static > Writable asWritable(final RegistryEntryInfo entry) { // TODO remove Keyed ++ return (Writable) possiblyUnwrap(entry); ++ } ++ } ++ ++ private static RegistryEntryInfo possiblyUnwrap(final RegistryEntryInfo entry) { ++ return entry instanceof final DelayedRegistryEntry delayed ? delayed.delegate() : entry; ++ } ++ + static RegistryEntry entry( + final ResourceKey> mcKey, + final RegistryKey apiKey, +@@ -48,4 +114,24 @@ public interface RegistryEntry extends RegistryEntryInfo(mcKey, apiKey, apiRegistrySupplier); + } ++ ++ static > RegistryEntry modifiable( ++ final ResourceKey> mcKey, ++ final RegistryKey apiKey, ++ final Class toPreload, ++ final BiFunction minecraftToBukkit, ++ final PaperRegistryBuilder.Filler filler ++ ) { ++ return new ModifiableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler); ++ } ++ ++ static > RegistryEntry writable( ++ final ResourceKey> mcKey, ++ final RegistryKey apiKey, ++ final Class toPreload, ++ final BiFunction minecraftToBukkit, ++ final PaperRegistryBuilder.Filler filler ++ ) { ++ return new WritableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler); ++ } + } +diff --git a/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..562accce731630327d116afd1c9d559df7e386bd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.registry.entry; ++ ++import io.papermc.paper.registry.PaperRegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import java.util.function.BiFunction; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++ ++public class WritableRegistryEntry> extends AddableRegistryEntry implements RegistryEntry.Writable { // TODO remove Keyed ++ ++ protected WritableRegistryEntry( ++ final ResourceKey> mcKey, ++ final RegistryKey apiKey, ++ final Class classToPreload, ++ final BiFunction minecraftToBukkit, ++ final PaperRegistryBuilder.Filler builderFiller ++ ) { ++ super(mcKey, apiKey, classToPreload, minecraftToBukkit, builderFiller); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc9c8fd313f530777af80ad79e03903f3f8f9829 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.registry.event; ++ ++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.set.NamedRegistryKeySetImpl; ++import io.papermc.paper.registry.tag.Tag; ++import io.papermc.paper.registry.tag.TagKey; ++import net.minecraft.core.HolderSet; ++import net.minecraft.resources.RegistryOps; ++import org.bukkit.Keyed; ++import org.checkerframework.checker.nullness.qual.NonNull; ++ ++public record RegistryEntryAddEventImpl>( ++ TypedKey key, ++ B builder, ++ RegistryKey registryKey, ++ Conversions conversions ++) implements RegistryEntryAddEvent, PaperLifecycleEvent { ++ ++ @Override ++ public @NonNull Tag getOrCreateTag(final TagKey tagKey) { ++ final RegistryOps.RegistryInfo registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow(); ++ final HolderSet.Named tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey)); ++ return new NamedRegistryKeySetImpl<>(tagKey, tagSet); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f5ea23173dcbe491742c3dd051c147ef397307a0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java +@@ -0,0 +1,44 @@ ++package io.papermc.paper.registry.event; ++ ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; ++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.Objects; ++import java.util.function.BiFunction; ++ ++public final class RegistryEventMap { ++ ++ private final Map, LifecycleEventType, ?>> hooks = new HashMap<>(); ++ private final String name; ++ ++ public RegistryEventMap(final String name) { ++ this.name = name; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public , E extends RegistryEvent, ET extends LifecycleEventType> ET getOrCreate(final RegistryEventProvider type, final BiFunction, ? super String, ET> eventTypeCreator) { ++ final ET registerHook; ++ if (this.hooks.containsKey(type.registryKey())) { ++ registerHook = (ET) this.hooks.get(type.registryKey()); ++ } else { ++ registerHook = eventTypeCreator.apply(type, this.name); ++ LifecycleEventRunner.INSTANCE.addEventType(registerHook); ++ this.hooks.put(type.registryKey(), registerHook); ++ } ++ return registerHook; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public > LifecycleEventType getHook(final RegistryKey registryKey) { ++ return (LifecycleEventType) Objects.requireNonNull(this.hooks.get(registryKey), "No hook for " + registryKey); ++ } ++ ++ public boolean hasHooks(final RegistryKey registryKey) { ++ return this.hooks.containsKey(registryKey); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..34c842ffa355e3c8001dd7b8551bcb49229a6391 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java +@@ -0,0 +1,24 @@ ++package io.papermc.paper.registry.event; ++ ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; ++import io.papermc.paper.registry.PaperRegistryListenerManager; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType; ++ ++public class RegistryEventTypeProviderImpl implements RegistryEventTypeProvider { ++ ++ public static RegistryEventTypeProviderImpl instance() { ++ return (RegistryEventTypeProviderImpl) RegistryEventTypeProvider.provider(); ++ } ++ ++ @Override ++ public > RegistryEntryAddEventType registryEntryAdd(final RegistryEventProvider type) { ++ return PaperRegistryListenerManager.INSTANCE.getRegistryValueAddEventType(type); ++ } ++ ++ @Override ++ public > LifecycleEventType.Prioritizable> registryFreeze(final RegistryEventProvider type) { ++ return PaperRegistryListenerManager.INSTANCE.getRegistryFreezeEventType(type); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..63957d2509e68ccc6eb2fd9ecaa35bfad7b71b81 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.registry.event; ++ ++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.set.NamedRegistryKeySetImpl; ++import io.papermc.paper.registry.tag.Tag; ++import io.papermc.paper.registry.tag.TagKey; ++import net.minecraft.core.HolderSet; ++import net.minecraft.resources.RegistryOps; ++import org.bukkit.Keyed; ++import org.checkerframework.checker.nullness.qual.NonNull; ++ ++public record RegistryFreezeEventImpl>( ++ RegistryKey registryKey, ++ WritableRegistry registry, ++ Conversions conversions ++) implements RegistryFreezeEvent, PaperLifecycleEvent { ++ ++ @Override ++ public @NonNull Tag getOrCreateTag(final TagKey tagKey) { ++ final RegistryOps.RegistryInfo registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow(); ++ final HolderSet.Named tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey)); ++ return new NamedRegistryKeySetImpl<>(tagKey, tagSet); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/package-info.java b/src/main/java/io/papermc/paper/registry/event/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..14d2d9766b8dee763f220c397aba3ad432d02aaa +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/package-info.java +@@ -0,0 +1,5 @@ ++@DefaultQualifier(NonNull.class) ++package io.papermc.paper.registry.event; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; +diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5d709ed04e1078b631f5b9c74ca35f042251e14f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.registry.event.type; ++ ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.event.RegistryEntryAddEvent; ++import io.papermc.paper.registry.event.RegistryEventProvider; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++ ++public class RegistryEntryAddEventTypeImpl> extends PrioritizableLifecycleEventType, RegistryEntryAddConfiguration> implements RegistryEntryAddEventType { ++ ++ public RegistryEntryAddEventTypeImpl(final RegistryEventProvider type, final String eventName) { ++ super(type.registryKey() + " / " + eventName, BootstrapContext.class); ++ } ++ ++ @Override ++ public RegistryEntryAddConfiguration newHandler(final LifecycleEventHandler> handler) { ++ return new RegistryEntryAddHandlerConfiguration<>(handler, this); ++ } ++ ++ @Override ++ public void forEachHandler(final RegistryEntryAddEvent event, final Consumer>> consumer, final Predicate>> predicate) { ++ super.forEachHandler(event, consumer, predicate.and(handler -> this.matchesTarget(event, handler))); ++ } ++ ++ private boolean matchesTarget(final RegistryEntryAddEvent event, final RegisteredHandler> handler) { ++ final RegistryEntryAddHandlerConfiguration config = (RegistryEntryAddHandlerConfiguration) handler.config(); ++ return config.filter() == null || config.filter().test(event.key()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..548f5bf979e88708e98d04dfe22ccaa300c91ddd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java +@@ -0,0 +1,42 @@ ++package io.papermc.paper.registry.event.type; ++ ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl; ++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.event.RegistryEntryAddEvent; ++import java.util.function.Predicate; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.Contract; ++ ++public class RegistryEntryAddHandlerConfiguration> extends PrioritizedLifecycleEventHandlerConfigurationImpl> implements RegistryEntryAddConfiguration { ++ ++ private @Nullable Predicate> filter; ++ ++ public RegistryEntryAddHandlerConfiguration(final LifecycleEventHandler> handler, final AbstractLifecycleEventType, ?> eventType) { ++ super(handler, eventType); ++ } ++ ++ @Contract(pure = true) ++ public @Nullable Predicate> filter() { ++ return this.filter; ++ } ++ ++ @Override ++ public RegistryEntryAddConfiguration filter(final Predicate> filter) { ++ this.filter = filter; ++ return this; ++ } ++ ++ @Override ++ public RegistryEntryAddConfiguration priority(final int priority) { ++ return (RegistryEntryAddConfiguration) super.priority(priority); ++ } ++ ++ @Override ++ public RegistryEntryAddConfiguration monitor() { ++ return (RegistryEntryAddConfiguration) super.monitor(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dcc0f6b337840a78d38abdf2eb3f4bbd1676f58f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java +@@ -0,0 +1,14 @@ ++package io.papermc.paper.registry.event.type; ++ ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType; ++import io.papermc.paper.registry.RegistryBuilder; ++import io.papermc.paper.registry.event.RegistryEvent; ++import io.papermc.paper.registry.event.RegistryEventProvider; ++ ++public final class RegistryLifecycleEventType, E extends RegistryEvent> extends PrioritizableLifecycleEventType.Simple { ++ ++ public RegistryLifecycleEventType(final RegistryEventProvider type, final String eventName) { ++ super(type.registryKey() + " / " + eventName, BootstrapContext.class); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java +index 5562e8da5ebaef2a3add46e88d64358b7737b59e..e5880f76cdb8ebf01fcefdf77ba9b95674b997a8 100644 +--- a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java ++++ b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java +@@ -1,12 +1,13 @@ + package io.papermc.paper.registry.legacy; + ++import io.papermc.paper.registry.tag.Tag; ++import io.papermc.paper.registry.tag.TagKey; + import java.util.Iterator; + import java.util.function.Supplier; + import java.util.stream.Stream; + import org.bukkit.Keyed; + import org.bukkit.NamespacedKey; + import org.bukkit.Registry; +-import org.bukkit.craftbukkit.CraftRegistry; + import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + import org.checkerframework.checker.nullness.qual.Nullable; + import org.jetbrains.annotations.NotNull; +@@ -52,4 +53,14 @@ public final class DelayedRegistry> imple + public NamespacedKey getKey(final T value) { + return this.delegate().getKey(value); + } ++ ++ @Override ++ public boolean hasTag(final TagKey key) { ++ return this.delegate().hasTag(key); ++ } ++ ++ @Override ++ public @NotNull Tag getTag(final TagKey key) { ++ return this.delegate().getTag(key); ++ } + } +diff --git a/src/main/java/io/papermc/paper/registry/package-info.java b/src/main/java/io/papermc/paper/registry/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0b80179ff90e085568d7ceafd9b17511789dc99b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/package-info.java +@@ -0,0 +1,5 @@ ++@DefaultQualifier(NonNull.class) ++package io.papermc.paper.registry; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; +diff --git a/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e8c2c18a1ed5cd587266bd415170610781531a12 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java +@@ -0,0 +1,76 @@ ++package io.papermc.paper.registry.set; ++ ++import com.google.common.collect.ImmutableList; ++import com.google.common.collect.Iterables; ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.RegistryAccess; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.tag.Tag; ++import io.papermc.paper.registry.tag.TagKey; ++import java.util.Collection; ++import java.util.Set; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.Holder; ++import net.minecraft.core.HolderSet; ++import org.bukkit.Keyed; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Registry; ++import org.bukkit.craftbukkit.util.CraftNamespacedKey; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Unmodifiable; ++ ++@DefaultQualifier(NonNull.class) ++public record NamedRegistryKeySetImpl( // TODO remove Keyed ++ TagKey tagKey, ++ HolderSet.Named namedSet ++) implements Tag, org.bukkit.Tag { ++ ++ @Override ++ public @Unmodifiable Collection> values() { ++ final ImmutableList.Builder> builder = ImmutableList.builder(); ++ for (final Holder holder : this.namedSet) { ++ builder.add(TypedKey.create(this.tagKey.registryKey(), CraftNamespacedKey.fromMinecraft(((Holder.Reference) holder).key().location()))); ++ } ++ return builder.build(); ++ } ++ ++ @Override ++ public RegistryKey registryKey() { ++ return this.tagKey.registryKey(); ++ } ++ ++ @Override ++ public boolean contains(final TypedKey valueKey) { ++ return Iterables.any(this.namedSet, h -> { ++ return PaperRegistries.fromNms(((Holder.Reference) h).key()).equals(valueKey); ++ }); ++ } ++ ++ @Override ++ public @Unmodifiable Collection resolve(final Registry registry) { ++ final ImmutableList.Builder builder = ImmutableList.builder(); ++ for (final Holder holder : this.namedSet) { ++ builder.add(registry.getOrThrow(CraftNamespacedKey.fromMinecraft(((Holder.Reference) holder).key().location()))); ++ } ++ return builder.build(); ++ } ++ ++ @Override ++ public boolean isTagged(final T item) { ++ return this.getValues().contains(item); ++ } ++ ++ @Override ++ public Set getValues() { ++ return Set.copyOf(this.resolve(RegistryAccess.registryAccess().getRegistry(this.registryKey()))); ++ } ++ ++ @Override ++ public @NotNull NamespacedKey getKey() { ++ final Key key = this.tagKey().key(); ++ return new NamespacedKey(key.namespace(), key.value()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f09ce9c8547ef05153847245746473dd9a8acbe6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java +@@ -0,0 +1,48 @@ ++package io.papermc.paper.registry.set; ++ ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import java.util.ArrayList; ++import java.util.List; ++import net.minecraft.core.Holder; ++import net.minecraft.core.HolderSet; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.Keyed; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperRegistrySets { ++ ++ public static HolderSet convertToNms(final ResourceKey> resourceKey, final RegistryOps.RegistryInfoLookup lookup, final RegistryKeySet registryKeySet) { // TODO remove Keyed ++ if (registryKeySet instanceof NamedRegistryKeySetImpl) { ++ return ((NamedRegistryKeySetImpl) registryKeySet).namedSet(); ++ } else { ++ final RegistryOps.RegistryInfo registryInfo = lookup.lookup(resourceKey).orElseThrow(); ++ return HolderSet.direct(key -> { ++ return registryInfo.getter().getOrThrow(PaperRegistries.toNms(key)); ++ }, registryKeySet.values()); ++ } ++ } ++ ++ public static RegistryKeySet convertToApi(final RegistryKey registryKey, final HolderSet holders) { // TODO remove Keyed ++ if (holders instanceof final HolderSet.Named named) { ++ return new NamedRegistryKeySetImpl<>(PaperRegistries.fromNms(named.key()), named); ++ } else { ++ final List> keys = new ArrayList<>(); ++ for (final Holder holder : holders) { ++ if (!(holder instanceof final Holder.Reference reference)) { ++ throw new UnsupportedOperationException("Cannot convert a holder set containing direct holders"); ++ } ++ keys.add(PaperRegistries.fromNms(reference.key())); ++ } ++ return RegistrySet.keySet(registryKey, keys); ++ } ++ } ++ ++ private PaperRegistrySets() { ++ } ++} +diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java +index edbbafd1705345282e5e6251eb71bfde5793b7d4..f22d22ebcedcc9c20225677844c86a1ad27c4211 100644 +--- a/src/main/java/net/minecraft/core/MappedRegistry.java ++++ b/src/main/java/net/minecraft/core/MappedRegistry.java +@@ -441,4 +441,12 @@ public class MappedRegistry implements WritableRegistry { + public HolderLookup.RegistryLookup asLookup() { + return this.lookup; + } ++ // Paper start ++ // used to clear intrusive holders from GameEvent, Item, Block, EntityType, and Fluid from unused instances of those types ++ public void clearIntrusiveHolder(final T instance) { ++ if (this.unregisteredIntrusiveHolders != null) { ++ this.unregisteredIntrusiveHolders.remove(instance); ++ } ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +index 44b7927081b476813505cab6b3a2da2ec2942c54..0497318e8f647453f38f3a16a8be6bd9aa19253f 100644 +--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java ++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +@@ -288,6 +288,17 @@ public class BuiltInRegistries { + Registries.ENCHANTMENT_PROVIDER_TYPE, EnchantmentProviderTypes::bootstrap + ); + public static final Registry> REGISTRY = WRITABLE_REGISTRY; ++ // Paper start - add built-in registry conversions ++ public static final io.papermc.paper.registry.data.util.Conversions BUILT_IN_CONVERSIONS = new io.papermc.paper.registry.data.util.Conversions(new net.minecraft.resources.RegistryOps.RegistryInfoLookup() { ++ @Override ++ public java.util.Optional> lookup(final ResourceKey> registryRef) { ++ final Registry registry = net.minecraft.server.RegistryLayer.STATIC_ACCESS.registryOrThrow(registryRef); ++ return java.util.Optional.of( ++ new net.minecraft.resources.RegistryOps.RegistryInfo<>(registry.asLookup(), registry.asTagAddingLookup(), Lifecycle.experimental()) ++ ); ++ } ++ }); ++ // Paper end - add built-in registry conversions + + private static Registry registerSimple(ResourceKey> key, BuiltInRegistries.RegistryBootstrap initializer) { + return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), initializer); +@@ -328,6 +339,7 @@ public class BuiltInRegistries { + } + public static void bootStrap(Runnable runnable) { + // Paper end ++ REGISTRY.freeze(); // Paper - freeze main registry early + createContents(); + runnable.run(); // Paper + freeze(); +@@ -346,6 +358,7 @@ public class BuiltInRegistries { + REGISTRY.freeze(); + + for (Registry registry : REGISTRY) { ++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), BUILT_IN_CONVERSIONS); // Paper + registry.freeze(); + } + } +diff --git a/src/main/java/net/minecraft/resources/RegistryDataLoader.java b/src/main/java/net/minecraft/resources/RegistryDataLoader.java +index abadf4abe08dc3bb6612b42cbb3f7df3ffa28ce9..3053243866c655829fe2e980446b4abf1da6d37c 100644 +--- a/src/main/java/net/minecraft/resources/RegistryDataLoader.java ++++ b/src/main/java/net/minecraft/resources/RegistryDataLoader.java +@@ -115,7 +115,7 @@ public class RegistryDataLoader { + ); + + public static RegistryAccess.Frozen load(ResourceManager resourceManager, RegistryAccess registryManager, List> entries) { +- return load((loader, infoGetter) -> loader.loadFromResources(resourceManager, infoGetter), registryManager, entries); ++ return load((loader, infoGetter, conversions) -> loader.loadFromResources(resourceManager, infoGetter, conversions), registryManager, entries); // Paper - pass conversions + } + + public static RegistryAccess.Frozen load( +@@ -124,7 +124,7 @@ public class RegistryDataLoader { + RegistryAccess registryManager, + List> entries + ) { +- return load((loader, infoGetter) -> loader.loadFromNetwork(data, factory, infoGetter), registryManager, entries); ++ return load((loader, infoGetter, conversions) -> loader.loadFromNetwork(data, factory, infoGetter, conversions), registryManager, entries); // Paper - pass conversions + } + + private static RegistryAccess.Frozen load( +@@ -133,9 +133,11 @@ public class RegistryDataLoader { + Map, Exception> map = new HashMap<>(); + List> list = entries.stream().map(entry -> entry.create(Lifecycle.stable(), map)).collect(Collectors.toUnmodifiableList()); + RegistryOps.RegistryInfoLookup registryInfoLookup = createContext(baseRegistryManager, list); +- list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader)loader, registryInfoLookup)); ++ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryInfoLookup); // Paper - create conversions ++ list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader)loader, registryInfoLookup, conversions)); + list.forEach(loader -> { + Registry registry = loader.registry(); ++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(loader.registry.key(), conversions); // Paper - run pre-freeze listeners + + try { + registry.freeze(); +@@ -193,13 +195,13 @@ public class RegistryDataLoader { + } + + private static void loadElementFromResource( +- WritableRegistry registry, Decoder decoder, RegistryOps ops, ResourceKey key, Resource resource, RegistrationInfo entryInfo ++ WritableRegistry registry, Decoder decoder, RegistryOps ops, ResourceKey key, Resource resource, RegistrationInfo entryInfo, io.papermc.paper.registry.data.util.Conversions conversions + ) throws IOException { + try (Reader reader = resource.openAsReader()) { + JsonElement jsonElement = JsonParser.parseReader(reader); + DataResult dataResult = decoder.parse(ops, jsonElement); + E object = dataResult.getOrThrow(); +- registry.register(key, object, entryInfo); ++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(registry, key, object, entryInfo, conversions); // Paper - register with listeners + } + } + +@@ -208,7 +210,8 @@ public class RegistryDataLoader { + RegistryOps.RegistryInfoLookup infoGetter, + WritableRegistry registry, + Decoder elementDecoder, +- Map, Exception> errors ++ Map, Exception> errors, ++ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions + ) { + String string = Registries.elementsDirPath(registry.key()); + FileToIdConverter fileToIdConverter = FileToIdConverter.json(string); +@@ -221,7 +224,7 @@ public class RegistryDataLoader { + RegistrationInfo registrationInfo = REGISTRATION_INFO_CACHE.apply(resource.knownPackInfo()); + + try { +- loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo); ++ loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo, conversions); // Paper - pass conversions + } catch (Exception var15) { + errors.put( + resourceKey, +@@ -237,7 +240,8 @@ public class RegistryDataLoader { + RegistryOps.RegistryInfoLookup infoGetter, + WritableRegistry registry, + Decoder decoder, +- Map, Exception> loadingErrors ++ Map, Exception> loadingErrors, ++ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions + ) { + List list = data.get(registry.key()); + if (list != null) { +@@ -264,7 +268,7 @@ public class RegistryDataLoader { + + try { + Resource resource = factory.getResourceOrThrow(resourceLocation); +- loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO); ++ loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO, conversions); // Paper - pass conversions + } catch (Exception var18) { + loadingErrors.put(resourceKey, new IllegalStateException("Failed to parse local data", var18)); + } +@@ -274,22 +278,23 @@ public class RegistryDataLoader { + } + + static record Loader(RegistryDataLoader.RegistryData data, WritableRegistry registry, Map, Exception> loadingErrors) { +- public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter) { +- RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors); ++ public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions) { // Paper - pass conversions ++ RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions + } + + public void loadFromNetwork( + Map>, List> data, + ResourceProvider factory, +- RegistryOps.RegistryInfoLookup infoGetter ++ RegistryOps.RegistryInfoLookup infoGetter, ++ io.papermc.paper.registry.data.util.Conversions conversions // Paper + ) { +- RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors); ++ RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions + } + } + + @FunctionalInterface + interface LoadingFunction { +- void apply(RegistryDataLoader.Loader loader, RegistryOps.RegistryInfoLookup infoGetter); ++ void apply(RegistryDataLoader.Loader loader, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions); // Paper - pass conversions + } + + public static record RegistryData(ResourceKey> key, Codec elementCodec, boolean requiredNonEmpty) { +diff --git a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java +index 397bdacab9517354875ebc0bc68d35059b3c318b..908431652a0fea79b5a0cee1efd0c7a7d524b614 100644 +--- a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java ++++ b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java +@@ -47,15 +47,16 @@ public class ReloadableServerRegistries { + ) { + RegistryAccess.Frozen frozen = dynamicRegistries.getAccessForLoading(RegistryLayer.RELOADABLE); + RegistryOps registryOps = new ReloadableServerRegistries.EmptyTagLookupWrapper(frozen).createSerializationContext(JsonOps.INSTANCE); ++ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryOps.lookupProvider); // Paper + List>> list = LootDataType.values() +- .map(type -> scheduleElementParse((LootDataType)type, registryOps, resourceManager, prepareExecutor)) ++ .map(type -> scheduleElementParse((LootDataType)type, registryOps, resourceManager, prepareExecutor, conversions)) // Paper + .toList(); + CompletableFuture>> completableFuture = Util.sequence(list); + return completableFuture.thenApplyAsync(registries -> apply(dynamicRegistries, (List>)registries), prepareExecutor); + } + + private static CompletableFuture> scheduleElementParse( +- LootDataType type, RegistryOps ops, ResourceManager resourceManager, Executor prepareExecutor ++ LootDataType type, RegistryOps ops, ResourceManager resourceManager, Executor prepareExecutor, io.papermc.paper.registry.data.util.Conversions conversions // Paper + ) { + return CompletableFuture.supplyAsync( + () -> { +@@ -66,7 +67,7 @@ public class ReloadableServerRegistries { + SimpleJsonResourceReloadListener.scanDirectory(resourceManager, string, GSON, map); + map.forEach( + (id, json) -> type.deserialize(id, ops, json) +- .ifPresent(value -> writableRegistry.register(ResourceKey.create(type.registryKey(), id), (T)value, DEFAULT_REGISTRATION_INFO)) ++ .ifPresent(value -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, ResourceKey.create(type.registryKey(), id), value, DEFAULT_REGISTRATION_INFO, conversions)) // Paper - register with listeners + ); + return writableRegistry; + }, +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +index d21b7e39d71c785f47f790e1ad4be33a8e8e6e51..a47421425a8d5d2f07e08890fded0f7bfec4efb7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +@@ -156,11 +156,11 @@ public class CraftRegistry implements Registry { + private final Map cache = new HashMap<>(); + private final Map byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry + private final net.minecraft.core.Registry minecraftRegistry; +- private final BiFunction minecraftToBukkit; ++ private final BiFunction minecraftToBukkit; // Paper + private final BiFunction serializationUpdater; // Paper - rename to make it *clear* what it is *only* for + private boolean init; + +- public CraftRegistry(Class bukkitClass, net.minecraft.core.Registry minecraftRegistry, BiFunction minecraftToBukkit, BiFunction serializationUpdater) { // Paper - relax preload class ++ public CraftRegistry(Class bukkitClass, net.minecraft.core.Registry minecraftRegistry, BiFunction minecraftToBukkit, BiFunction serializationUpdater) { // Paper - relax preload class + this.bukkitClass = bukkitClass; + this.minecraftRegistry = minecraftRegistry; + this.minecraftToBukkit = minecraftToBukkit; +@@ -233,4 +233,17 @@ public class CraftRegistry implements Registry { + return this.byValue.get(value); + } + // Paper end - improve Registry ++ ++ // Paper start - RegistrySet API ++ @Override ++ public boolean hasTag(final io.papermc.paper.registry.tag.TagKey key) { ++ return this.minecraftRegistry.getTag(net.minecraft.tags.TagKey.create(this.minecraftRegistry.key(), io.papermc.paper.adventure.PaperAdventure.asVanilla(key.key()))).isPresent(); ++ } ++ ++ @Override ++ public io.papermc.paper.registry.tag.Tag getTag(final io.papermc.paper.registry.tag.TagKey key) { ++ final net.minecraft.core.HolderSet.Named namedHolderSet = this.minecraftRegistry.getTag(io.papermc.paper.registry.PaperRegistries.toNms(key)).orElseThrow(); ++ return new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(key, namedHolderSet); ++ } ++ // Paper end - RegistrySet API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 5a04134973dd1db7f778a57ec5f185feec370990..8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -685,6 +685,21 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end - lifecycle event API + ++ // Paper start - hack to get tags for non server-backed registries ++ @Override ++ public io.papermc.paper.registry.tag.Tag getTag(final io.papermc.paper.registry.tag.TagKey tagKey) { // TODO remove Keyed ++ if (tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.ENTITY_TYPE || tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.FLUID) { ++ throw new UnsupportedOperationException(tagKey.registryKey() + " doesn't have tags"); ++ } ++ final net.minecraft.resources.ResourceKey> nmsKey = io.papermc.paper.registry.PaperRegistries.registryToNms(tagKey.registryKey()); ++ final net.minecraft.core.Registry nmsRegistry = org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry().registryOrThrow(nmsKey); ++ return nmsRegistry ++ .getTag(io.papermc.paper.registry.PaperRegistries.toNms(tagKey)) ++ .map(named -> new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(tagKey, named)) ++ .orElse(null); ++ } ++ // Paper end - hack to get tags for non server-backed registries ++ + /** + * This helper class represents the different NBT Tags. + *

+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider +new file mode 100644 +index 0000000000000000000000000000000000000000..8bee1a5ed877a04e4d027593df1f42cefdd824e7 +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider +@@ -0,0 +1 @@ ++io.papermc.paper.registry.event.RegistryEventTypeProviderImpl +diff --git a/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f27e5e0037b719b1fc10703f8d298d2326b00432 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.registry; ++ ++import io.papermc.paper.registry.data.util.Conversions; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.resources.ResourceKey; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.Disabled; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.Arguments; ++import org.junit.jupiter.params.provider.MethodSource; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++ ++class RegistryBuilderTest extends AbstractTestingBase { ++ ++ static List registries() { ++ return List.of( ++ ); ++ } ++ ++ @Disabled ++ @ParameterizedTest ++ @MethodSource("registries") ++ void testEquality(final ResourceKey> resourceKey, final PaperRegistryBuilder.Filler filler) { ++ final Registry registry = AbstractTestingBase.REGISTRY_CUSTOM.registryOrThrow(resourceKey); ++ for (final Map.Entry, M> entry : registry.entrySet()) { ++ final M built = filler.fill(new Conversions(new RegistryOps.HolderLookupAdapter(AbstractTestingBase.REGISTRY_CUSTOM)), PaperRegistries.fromNms(entry.getKey()), entry.getValue()).build(); ++ assertEquals(entry.getValue(), built); ++ } ++ } ++} diff --git a/patches/server/1026-Add-registry-entry-and-builders.patch b/patches/server/1026-Add-registry-entry-and-builders.patch new file mode 100644 index 0000000000..1b920c14d0 --- /dev/null +++ b/patches/server/1026-Add-registry-entry-and-builders.patch @@ -0,0 +1,483 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bjarne Koll +Date: Thu, 13 Jun 2024 23:45:32 +0200 +Subject: [PATCH] Add registry entry and builders + + +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +index a688af29273ebfbb4f75dd74cd30627fc481c96c..9c0972023bc9be20ba81bbc2e1d6688b615760c0 100644 +--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +@@ -1,6 +1,8 @@ + package io.papermc.paper.registry; + + import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry; ++import io.papermc.paper.registry.data.PaperGameEventRegistryEntry; + import io.papermc.paper.registry.entry.RegistryEntry; + import io.papermc.paper.registry.tag.TagKey; + import java.util.Collections; +@@ -58,7 +60,7 @@ public final class PaperRegistries { + static { + REGISTRY_ENTRIES = List.of( + // built-ins +- entry(Registries.GAME_EVENT, RegistryKey.GAME_EVENT, GameEvent.class, CraftGameEvent::new), ++ writable(Registries.GAME_EVENT, RegistryKey.GAME_EVENT, GameEvent.class, CraftGameEvent::new, PaperGameEventRegistryEntry.PaperBuilder::new), + entry(Registries.INSTRUMENT, RegistryKey.INSTRUMENT, MusicInstrument.class, CraftMusicInstrument::new), + entry(Registries.MOB_EFFECT, RegistryKey.MOB_EFFECT, PotionEffectType.class, CraftPotionEffectType::new), + entry(Registries.STRUCTURE_TYPE, RegistryKey.STRUCTURE_TYPE, StructureType.class, CraftStructureType::new), +@@ -71,7 +73,7 @@ public final class PaperRegistries { + entry(Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN, TrimPattern.class, CraftTrimPattern::new).delayed(), + entry(Registries.DAMAGE_TYPE, RegistryKey.DAMAGE_TYPE, DamageType.class, CraftDamageType::new).delayed(), + entry(Registries.WOLF_VARIANT, RegistryKey.WOLF_VARIANT, Wolf.Variant.class, CraftWolf.CraftVariant::new).delayed(), +- entry(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT, Enchantment.class, CraftEnchantment::new).withSerializationUpdater(FieldRename.ENCHANTMENT_RENAME).delayed(), ++ writable(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT, Enchantment.class, CraftEnchantment::new, PaperEnchantmentRegistryEntry.PaperBuilder::new).withSerializationUpdater(FieldRename.ENCHANTMENT_RENAME).delayed(), + entry(Registries.JUKEBOX_SONG, RegistryKey.JUKEBOX_SONG, JukeboxSong.class, CraftJukeboxSong::new).delayed(), + + // api-only +diff --git a/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b9501554a0738776c265852184c7dbae3fe98c8c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/data/PaperEnchantmentRegistryEntry.java +@@ -0,0 +1,234 @@ ++package io.papermc.paper.registry.data; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.Iterables; ++import com.google.common.collect.Lists; ++import io.papermc.paper.registry.PaperRegistryBuilder; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.TypedKey; ++import io.papermc.paper.registry.data.util.Checks; ++import io.papermc.paper.registry.data.util.Conversions; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import java.util.Collections; ++import java.util.List; ++import java.util.Optional; ++import java.util.OptionalInt; ++import net.minecraft.core.HolderSet; ++import net.minecraft.core.component.DataComponentMap; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.network.chat.Component; ++import net.minecraft.world.entity.EquipmentSlotGroup; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.enchantment.Enchantment; ++import org.bukkit.craftbukkit.CraftEquipmentSlot; ++import org.bukkit.inventory.ItemType; ++import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.Range; ++ ++import static io.papermc.paper.registry.data.util.Checks.asArgument; ++import static io.papermc.paper.registry.data.util.Checks.asArgumentMin; ++import static io.papermc.paper.registry.data.util.Checks.asConfigured; ++ ++@DefaultQualifier(NonNull.class) ++public class PaperEnchantmentRegistryEntry implements EnchantmentRegistryEntry { ++ ++ // Top level ++ protected @MonotonicNonNull Component description; ++ ++ // Definition ++ protected @MonotonicNonNull HolderSet supportedItems; ++ protected @Nullable HolderSet primaryItems; ++ protected OptionalInt weight = OptionalInt.empty(); ++ protected OptionalInt maxLevel = OptionalInt.empty(); ++ protected Enchantment.@MonotonicNonNull Cost minimumCost; ++ protected Enchantment.@MonotonicNonNull Cost maximumCost; ++ protected OptionalInt anvilCost = OptionalInt.empty(); ++ protected @MonotonicNonNull List activeSlots; ++ ++ // Exclusive ++ protected HolderSet exclusiveWith = HolderSet.empty(); // Paper added default to empty. ++ ++ // Effects ++ protected final DataComponentMap effects; ++ ++ protected final Conversions conversions; ++ ++ public PaperEnchantmentRegistryEntry( ++ final Conversions conversions, ++ final TypedKey ignoredKey, ++ final @Nullable Enchantment internal ++ ) { ++ this.conversions = conversions; ++ if (internal == null) { ++ this.effects = DataComponentMap.EMPTY; ++ return; ++ } ++ ++ // top level ++ this.description = internal.description(); ++ ++ // definition ++ final Enchantment.EnchantmentDefinition definition = internal.definition(); ++ this.supportedItems = definition.supportedItems(); ++ this.primaryItems = definition.primaryItems().orElse(null); ++ this.weight = OptionalInt.of(definition.weight()); ++ this.maxLevel = OptionalInt.of(definition.maxLevel()); ++ this.minimumCost = definition.minCost(); ++ this.maximumCost = definition.maxCost(); ++ this.anvilCost = OptionalInt.of(definition.anvilCost()); ++ this.activeSlots = definition.slots(); ++ ++ // exclusive ++ this.exclusiveWith = internal.exclusiveSet(); ++ ++ // effects ++ this.effects = internal.effects(); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component description() { ++ return this.conversions.asAdventure(asConfigured(this.description, "description")); ++ } ++ ++ @Override ++ public RegistryKeySet supportedItems() { ++ return PaperRegistrySets.convertToApi(RegistryKey.ITEM, asConfigured(this.supportedItems, "supportedItems")); ++ } ++ ++ @Override ++ public @Nullable RegistryKeySet primaryItems() { ++ return this.primaryItems == null ? null : PaperRegistrySets.convertToApi(RegistryKey.ITEM, this.primaryItems); ++ } ++ ++ @Override ++ public @Range(from = 1, to = 1024) int weight() { ++ return asConfigured(this.weight, "weight"); ++ } ++ ++ @Override ++ public @Range(from = 1, to = 255) int maxLevel() { ++ return asConfigured(this.maxLevel, "maxLevel"); ++ } ++ ++ @Override ++ public EnchantmentCost minimumCost() { ++ final Enchantment.@MonotonicNonNull Cost cost = asConfigured(this.minimumCost, "minimumCost"); ++ return EnchantmentRegistryEntry.EnchantmentCost.of(cost.base(), cost.perLevelAboveFirst()); ++ } ++ ++ @Override ++ public EnchantmentCost maximumCost() { ++ final Enchantment.@MonotonicNonNull Cost cost = asConfigured(this.maximumCost, "maximumCost"); ++ return EnchantmentRegistryEntry.EnchantmentCost.of(cost.base(), cost.perLevelAboveFirst()); ++ } ++ ++ @Override ++ public @Range(from = 0, to = Integer.MAX_VALUE) int anvilCost() { ++ return asConfigured(this.anvilCost, "anvilCost"); ++ } ++ ++ @Override ++ public List activeSlots() { ++ return Collections.unmodifiableList(Lists.transform(asConfigured(this.activeSlots, "activeSlots"), CraftEquipmentSlot::getSlot)); ++ } ++ ++ @Override ++ public RegistryKeySet exclusiveWith() { ++ return PaperRegistrySets.convertToApi(RegistryKey.ENCHANTMENT, this.exclusiveWith); ++ } ++ ++ public static final class PaperBuilder extends PaperEnchantmentRegistryEntry implements EnchantmentRegistryEntry.Builder, ++ PaperRegistryBuilder { ++ ++ public PaperBuilder(final Conversions conversions, final TypedKey key, final @Nullable Enchantment internal) { ++ super(conversions, key, internal); ++ } ++ ++ @Override ++ public Builder description(final net.kyori.adventure.text.Component description) { ++ this.description = this.conversions.asVanilla(asArgument(description, "description")); ++ return this; ++ } ++ ++ @Override ++ public Builder supportedItems(final RegistryKeySet supportedItems) { ++ this.supportedItems = PaperRegistrySets.convertToNms(Registries.ITEM, this.conversions.lookup(), asArgument(supportedItems, "supportedItems")); ++ return this; ++ } ++ ++ @Override ++ public Builder primaryItems(final @Nullable RegistryKeySet primaryItems) { ++ this.primaryItems = primaryItems == null ? null : PaperRegistrySets.convertToNms(Registries.ITEM, this.conversions.lookup(), primaryItems); ++ return this; ++ } ++ ++ @Override ++ public Builder weight(final @Range(from = 1, to = 1024) int weight) { ++ this.weight = OptionalInt.of(Checks.asArgumentRange(weight, "weight", 1, 1024)); ++ return this; ++ } ++ ++ @Override ++ public Builder maxLevel(final @Range(from = 1, to = 255) int maxLevel) { ++ this.maxLevel = OptionalInt.of(Checks.asArgumentRange(maxLevel, "maxLevel", 1, 255)); ++ return this; ++ } ++ ++ @Override ++ public Builder minimumCost(final EnchantmentCost minimumCost) { ++ final EnchantmentCost validCost = asArgument(minimumCost, "minimumCost"); ++ this.minimumCost = Enchantment.dynamicCost(validCost.baseCost(), validCost.additionalPerLevelCost()); ++ return this; ++ } ++ ++ @Override ++ public Builder maximumCost(final EnchantmentCost maximumCost) { ++ final EnchantmentCost validCost = asArgument(maximumCost, "maximumCost"); ++ this.maximumCost = Enchantment.dynamicCost(validCost.baseCost(), validCost.additionalPerLevelCost()); ++ return this; ++ } ++ ++ @Override ++ public Builder anvilCost(final @Range(from = 0, to = Integer.MAX_VALUE) int anvilCost) { ++ Preconditions.checkArgument(anvilCost >= 0, "anvilCost must be non-negative"); ++ this.anvilCost = OptionalInt.of(asArgumentMin(anvilCost, "anvilCost", 0)); ++ return this; ++ } ++ ++ @Override ++ public Builder activeSlots(final Iterable activeSlots) { ++ this.activeSlots = Lists.newArrayList(Iterables.transform(asArgument(activeSlots, "activeSlots"), CraftEquipmentSlot::getNMSGroup)); ++ return this; ++ } ++ ++ @Override ++ public Builder exclusiveWith(final RegistryKeySet exclusiveWith) { ++ this.exclusiveWith = PaperRegistrySets.convertToNms(Registries.ENCHANTMENT, this.conversions.lookup(), asArgument(exclusiveWith, "exclusiveWith")); ++ return this; ++ } ++ ++ @Override ++ public Enchantment build() { ++ final Enchantment.EnchantmentDefinition def = new Enchantment.EnchantmentDefinition( ++ asConfigured(this.supportedItems, "supportedItems"), ++ Optional.ofNullable(this.primaryItems), ++ this.weight(), ++ this.maxLevel(), ++ asConfigured(this.minimumCost, "minimumCost"), ++ asConfigured(this.maximumCost, "maximumCost"), ++ this.anvilCost(), ++ Collections.unmodifiableList(asConfigured(this.activeSlots, "activeSlots")) ++ ); ++ return new Enchantment( ++ asConfigured(this.description, "description"), ++ def, ++ this.exclusiveWith, ++ this.effects ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java b/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..18f9463ae23ba2d9c65ffb7531a87c925a5a8d6f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/data/PaperGameEventRegistryEntry.java +@@ -0,0 +1,57 @@ ++package io.papermc.paper.registry.data; ++ ++import io.papermc.paper.registry.PaperRegistryBuilder; ++import io.papermc.paper.registry.data.util.Conversions; ++import java.util.OptionalInt; ++import net.minecraft.world.level.gameevent.GameEvent; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.Range; ++ ++import static io.papermc.paper.registry.data.util.Checks.asArgumentMin; ++import static io.papermc.paper.registry.data.util.Checks.asConfigured; ++ ++@DefaultQualifier(NonNull.class) ++public class PaperGameEventRegistryEntry implements GameEventRegistryEntry { ++ ++ protected OptionalInt range = OptionalInt.empty(); ++ ++ public PaperGameEventRegistryEntry( ++ final Conversions ignoredConversions, ++ final io.papermc.paper.registry.TypedKey ignoredKey, ++ final @Nullable GameEvent nms ++ ) { ++ if (nms == null) return; ++ ++ this.range = OptionalInt.of(nms.notificationRadius()); ++ } ++ ++ @Override ++ public @Range(from = 0, to = Integer.MAX_VALUE) int range() { ++ return asConfigured(this.range, "range"); ++ } ++ ++ public static final class PaperBuilder extends PaperGameEventRegistryEntry implements GameEventRegistryEntry.Builder, ++ PaperRegistryBuilder { ++ ++ public PaperBuilder( ++ final Conversions conversions, ++ final io.papermc.paper.registry.TypedKey key, ++ final @Nullable GameEvent nms ++ ) { ++ super(conversions, key, nms); ++ } ++ ++ @Override ++ public GameEventRegistryEntry.Builder range(final @Range(from = 0, to = Integer.MAX_VALUE) int range) { ++ this.range = OptionalInt.of(asArgumentMin(range, "range", 0)); ++ return this; ++ } ++ ++ @Override ++ public GameEvent build() { ++ return new GameEvent(this.range()); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/registry/data/util/Checks.java b/src/main/java/io/papermc/paper/registry/data/util/Checks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a66cebe2b918a22b84e346127ffb671621050e5f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/registry/data/util/Checks.java +@@ -0,0 +1,48 @@ ++package io.papermc.paper.registry.data.util; ++ ++import java.util.OptionalInt; ++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 Checks { ++ ++ public static T asConfigured(final @Nullable T value, final String field) { ++ if (value == null) { ++ throw new IllegalStateException(field + " has not been configured"); ++ } ++ return value; ++ } ++ ++ public static int asConfigured(final OptionalInt value, final String field) { ++ if (value.isEmpty()) { ++ throw new IllegalStateException(field + " has not been configured"); ++ } ++ return value.getAsInt(); ++ } ++ ++ public static T asArgument(final @Nullable T value, final String field) { ++ if (value == null) { ++ throw new IllegalArgumentException("argument " + value + " cannot be null"); ++ } ++ return value; ++ } ++ ++ public static int asArgumentRange(final int value, final String field, final int min, final int max) { ++ if (value < min || value > max) { ++ throw new IllegalArgumentException("argument " + field + " must be [" + min + ", " + max + "]"); ++ } ++ return value; ++ } ++ ++ public static int asArgumentMin(final int value, final String field, final int min) { ++ if (value < min) { ++ throw new IllegalArgumentException("argument " + field + " must be [" + min + ",+inf)"); ++ } ++ return value; ++ } ++ ++ private Checks() { ++ } ++} +diff --git a/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java b/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java +index cc3c56224e64816b885c0131ce2a800a2efe3113..7acd9c71c5c4b487e792b8c36a8e52e10b691e98 100644 +--- a/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java ++++ b/src/main/java/net/minecraft/world/level/gameevent/GameEvent.java +@@ -85,7 +85,7 @@ public record GameEvent(int notificationRadius) { + } + + private static Holder.Reference register(String id, int range) { +- return Registry.registerForHolder(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(id), new GameEvent(range)); ++ return io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerForHolderWithListeners(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(id), new GameEvent(range)); // Paper - run with listeners + } + + public static record Context(@Nullable Entity sourceEntity, @Nullable BlockState affectedState) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java b/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java +index ac9b4328cd55a68664a3f71186bc9a7be7cd9658..ea9fe1f8b1a1685ea975eba0ca418a831006065a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftGameEvent.java +@@ -18,10 +18,12 @@ public class CraftGameEvent extends GameEvent implements Handleable handleKey; // Paper + private final net.minecraft.world.level.gameevent.GameEvent handle; + + public CraftGameEvent(NamespacedKey key, net.minecraft.world.level.gameevent.GameEvent handle) { + this.key = key; ++ this.handleKey = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.GAME_EVENT, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(key)); // Paper + this.handle = handle; + } + +@@ -30,6 +32,18 @@ public class CraftGameEvent extends GameEvent implements Handleable registries() { + return List.of( ++ arguments(Registries.ENCHANTMENT, (PaperRegistryBuilder.Filler) PaperEnchantmentRegistryEntry.PaperBuilder::new), ++ arguments(Registries.GAME_EVENT, (PaperRegistryBuilder.Filler) PaperGameEventRegistryEntry.PaperBuilder::new) + ); + } + +- @Disabled + @ParameterizedTest + @MethodSource("registries") + void testEquality(final ResourceKey> resourceKey, final PaperRegistryBuilder.Filler filler) { diff --git a/patches/server/1026-Fix-NPE-for-Jukebox-setRecord.patch b/patches/server/1026-Fix-NPE-for-Jukebox-setRecord.patch deleted file mode 100644 index e15a77ee75..0000000000 --- a/patches/server/1026-Fix-NPE-for-Jukebox-setRecord.patch +++ /dev/null @@ -1,20 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Mon, 17 Jun 2024 17:41:09 -0700 -Subject: [PATCH] Fix NPE for Jukebox#setRecord - -Fallback to the global registry if no level exists - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java -index 1497e76b548ad76b5aaa297bdd35723e6a8f1f8d..c5069730b25e6f0dfb4e5db3271c91116b485f58 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java -@@ -200,7 +200,7 @@ public class JukeboxBlockEntity extends BlockEntity implements Clearable, Contai - public void setSongItemWithoutPlaying(ItemStack itemstack, long ticksSinceSongStarted) { // CraftBukkit - add argument - this.item = itemstack; - this.jukeboxSongPlayer.song = null; // CraftBukkit - reset -- JukeboxSong.fromStack(this.level.registryAccess(), itemstack).ifPresent((holder) -> { -+ JukeboxSong.fromStack(this.level != null ? this.level.registryAccess() : org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry(), itemstack).ifPresent((holder) -> { // Paper - fallback to other RegistyrAccess if no level - this.jukeboxSongPlayer.setSongWithoutPlaying(holder, ticksSinceSongStarted); // CraftBukkit - add argument - }); - // CraftBukkit start - add null check for level diff --git a/patches/server/1027-Fix-CraftWorld-isChunkGenerated.patch b/patches/server/1027-Fix-CraftWorld-isChunkGenerated.patch deleted file mode 100644 index f91e8b2bd0..0000000000 --- a/patches/server/1027-Fix-CraftWorld-isChunkGenerated.patch +++ /dev/null @@ -1,44 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Tue, 18 Jun 2024 12:43:06 -0700 -Subject: [PATCH] Fix CraftWorld#isChunkGenerated - -The upstream implementation is returning true for non-full chunks. - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index fe9e8d1d4ae1f7a4e8f4cf5688004fc969422b3c..8045d6c9398d1c88595da6e41aa1ed27fb6fbad0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -367,11 +367,28 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public boolean isChunkGenerated(int x, int z) { -- try { -- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); -- } catch (InterruptedException | ExecutionException ex) { -- throw new RuntimeException(ex); -+ // Paper start - Fix this method -+ if (!Bukkit.isPrimaryThread()) { -+ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ return CraftWorld.this.isChunkGenerated(x, z); -+ }, world.getChunkSource().mainThreadProcessor).join(); -+ } -+ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); -+ if (chunk != null) { -+ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; - } -+ final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); -+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad( -+ this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, future::complete -+ ); -+ world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone); -+ return future.thenApply(c -> { -+ if (c != null) { -+ return c.getPersistedStatus() == ChunkStatus.FULL; -+ } -+ return false; -+ }).join(); -+ // Paper end - Fix this method - } - - @Override diff --git a/patches/server/1027-Improved-Watchdog-Support.patch b/patches/server/1027-Improved-Watchdog-Support.patch new file mode 100644 index 0000000000..cacfe63010 --- /dev/null +++ b/patches/server/1027-Improved-Watchdog-Support.patch @@ -0,0 +1,433 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sun, 12 Apr 2020 15:50:48 -0400 +Subject: [PATCH] Improved Watchdog Support + +Forced Watchdog Crash support and Improve Async Shutdown + +If the request to shut down the server is received while we are in +a watchdog hang, immediately treat it as a crash and begin the shutdown +process. Shutdown process is now improved to also shutdown cleanly when +not using restart scripts either. + +If a server is deadlocked, a server owner can send SIGUP (or any other signal +the JVM understands to shut down as it currently does) and the watchdog +will no longer need to wait until the full timeout, allowing you to trigger +a close process and try to shut the server down gracefully, saving player and +world data. + +Previously there was no way to trigger this outside of waiting for a full watchdog +timeout, which may be set to a really long time... + +Additionally, fix everything to do with shutting the server down asynchronously. + +Previously, nearly everything about the process was fragile and unsafe. Main might +not have actually been frozen, and might still be manipulating state. + +Or, some reuest might ask main to do something in the shutdown but main is dead. + +Or worse, other things might start closing down items such as the Console or Thread Pool +before we are fully shutdown. + +This change tries to resolve all of these issues by moving everything into the stop +method and guaranteeing only one thread is stopping the server. + +We then issue Thread Death to the main thread of another thread initiates the stop process. +We have to ensure Thread Death propagates correctly though to stop main completely. + +This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. + +This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they +are properly accounted for and wont trip watchdog on init. + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -92,7 +92,12 @@ public class Metrics { + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { +- final Runnable submitTask = this::submitData; ++ final Runnable submitTask = () -> { ++ if (MinecraftServer.getServer().hasStopped()) { ++ return; ++ } ++ 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. +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index 589a8bf75be6ccc59f1e5dd5d8d9afed41c4772d..b24265573fdef5d9a964bcd76146f34542c420cf 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -237,6 +237,7 @@ public class CrashReport { + } + + public static CrashReport forThrowable(Throwable cause, String title) { ++ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper + while (cause instanceof CompletionException && cause.getCause() != null) { + cause = cause.getCause(); + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 17b0e570016504d1b7704bbfa9ff2e3233b45d09..330bee331335454a61cf8350a6654217c8124445 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 processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + // Paper - don't store the vanilla dispatcher +- private boolean forceTicks; ++ public boolean forceTicks; // Paper - Improved watchdog support + // CraftBukkit end + // Spigot start + public static final int TPS = 20; +@@ -320,6 +320,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); + Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system +@@ -1005,6 +1008,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; ++ } ++ // Paper end + return new TickTask(this.tickCount, runnable); + } + +@@ -2267,7 +2319,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler + try { + task.run(); + } catch (Exception var3) { ++ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper + LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", this.name(), var3); + } + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 81f28afeea34d5e4ebb7f8fa7e3f5f124069a5ab..0d202ce8eb88bfdb8ca3306593d758fa483d8612 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -994,6 +994,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + try { + tickConsumer.accept(entity); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // 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); +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 b2a06fc1192cd6050d6d7ea8620a4fa5a12182cc..d388fbcbff63928f0e9140c02400a63ba8f19d9c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1006,6 +1006,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + gameprofilerfiller.pop(); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // 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); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { + @Override + public void run() { + try { ++ // Paper start - try to shutdown on main ++ server.safeShutdown(false, false); ++ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { ++ Thread.sleep(100); ++ } ++ if (server.hasStopped()) { ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ return; ++ } ++ // Looks stalled, close async + org.spigotmc.AsyncCatcher.enabled = false; // Spigot ++ server.forceTicks = true; + this.server.close(); ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ // Paper end + } finally { ++ org.apache.logging.log4j.LogManager.shutdown(); // Paper + try { +- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender ++ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop + } catch (Exception e) { + } + } +diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java +index f3fa0340babfc5eb627066115164e2d885c602c2..9ce945dce3ac8aa1d4eb55c41115f989b985d46d 100644 +--- a/src/main/java/org/spigotmc/RestartCommand.java ++++ b/src/main/java/org/spigotmc/RestartCommand.java +@@ -139,7 +139,7 @@ public class RestartCommand extends Command + // Paper end + + // Paper start - copied from above and modified to return if the hook registered +- private static boolean addShutdownHook(String restartScript) ++ public static boolean addShutdownHook(String restartScript) // Paper + { + String[] split = restartScript.split( " " ); + if ( split.length > 0 && new File( split[0] ).isFile() ) +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 7507e3058e7519a3e13b3be061746151a71b8f20..e5e41dc2d4f7a8c3fea704212507ca0b951664db 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; + public class WatchdogThread extends Thread + { + ++ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support + private static WatchdogThread instance; + private long timeoutTime; + private boolean restart; +@@ -39,6 +40,7 @@ public class WatchdogThread extends Thread + { + if ( WatchdogThread.instance == null ) + { ++ if (timeoutTime <= 0) timeoutTime = 300; // Paper + WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); + WatchdogThread.instance.start(); + } else +@@ -70,12 +72,13 @@ public class WatchdogThread extends Thread + // Paper start + Logger log = Bukkit.getServer().getLogger(); + long currentTime = WatchdogThread.monotonicMillis(); +- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable ++ MinecraftServer server = MinecraftServer.getServer(); ++ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable + { +- boolean isLongTimeout = currentTime > lastTick + timeoutTime; ++ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); + // Don't spam early warning dumps + if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; +- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... ++ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... + lastEarlyWarning = currentTime; + if (isLongTimeout) { + // Paper end +@@ -136,9 +139,24 @@ public class WatchdogThread extends Thread + + if ( isLongTimeout ) + { +- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) ++ if ( !server.hasStopped() ) + { +- RestartCommand.restart(); ++ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us ++ server.forceTicks = true; ++ if (restart) { ++ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); ++ } ++ // try one last chance to safe shutdown on main incase it 'comes back' ++ server.abnormalExit = true; ++ server.safeShutdown(false, restart); ++ try { ++ Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ } ++ if (!server.hasStopped()) { ++ server.close(); ++ } + } + break; + } // Paper end +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -1,5 +1,5 @@ + +- ++ + + + diff --git a/patches/server/1028-Add-debug-for-chunk-system-unload-crash.patch b/patches/server/1028-Add-debug-for-chunk-system-unload-crash.patch deleted file mode 100644 index 401b927f05..0000000000 --- a/patches/server/1028-Add-debug-for-chunk-system-unload-crash.patch +++ /dev/null @@ -1,57 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Wed, 19 Jun 2024 10:52:07 -0700 -Subject: [PATCH] Add debug for chunk system unload crash - -Somehow, a chunkholder is present in the unload queue after -it has been unloaded. It is likely that this is a result of -adding the chunk holder to the unload queue while it is -unloading. However, that should not be possible. - -To find out where it is being added to the unload queue, track -the last stacktrace which adds to the unload queue and check -on chunk holder remove if the holder is present in the unload queue -and log the stacktrace. - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -index d5fc5756ea960096ff23376a6b7ac68a2a462d22..0715514c8bb49c805b04f42ff37903a7d590aaa8 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -@@ -748,9 +748,22 @@ public final class NewChunkHolder { - - /** Unloaded from chunk map */ - private boolean unloaded; -+ private Throwable lastUnloadAdd; - - void markUnloaded() { - this.unloaded = true; -+ if (this.inUnloadQueue) { -+ if (this.lastUnloadAdd != null) { -+ LOGGER.error("Unloaded chunkholder " + this.toString() + " while in the unload queue", this.lastUnloadAdd); -+ } else { -+ // should never happen -+ LOGGER.error("Unloaded chunkholder " + this.toString() + " while in the unload queue without a throwable"); -+ } -+ -+ // prevent crash by removing (note: we hold scheduling lock here) -+ this.inUnloadQueue = false; -+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); -+ } - } - - private boolean inUnloadQueue = false; -@@ -768,12 +781,14 @@ public final class NewChunkHolder { - // ensure in unload queue - if (!this.inUnloadQueue) { - this.inUnloadQueue = true; -+ this.lastUnloadAdd = new Throwable(); - this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); - } - } else { - // ensure not in unload queue - if (this.inUnloadQueue) { - this.inUnloadQueue = false; -+ this.lastUnloadAdd = null; - this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); - } - } diff --git a/patches/server/1028-Proxy-ItemStack-to-CraftItemStack.patch b/patches/server/1028-Proxy-ItemStack-to-CraftItemStack.patch new file mode 100644 index 0000000000..aba1d9b341 --- /dev/null +++ b/patches/server/1028-Proxy-ItemStack-to-CraftItemStack.patch @@ -0,0 +1,300 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Tue, 14 May 2024 11:57:43 -0700 +Subject: [PATCH] Proxy ItemStack to CraftItemStack + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index be36336a3c7d1ae88277f4ee1be70075001de7a7..814e8ece6821e359b504e8c4d140cc38700f2abe 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -25,15 +25,57 @@ import org.bukkit.material.MaterialData; + @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; ++ // Paper start - delegate api-ItemStack to CraftItemStack ++ private static final java.lang.invoke.VarHandle API_ITEM_STACK_CRAFT_DELEGATE_FIELD; ++ static { ++ try { ++ API_ITEM_STACK_CRAFT_DELEGATE_FIELD = java.lang.invoke.MethodHandles.privateLookupIn( ++ ItemStack.class, ++ java.lang.invoke.MethodHandles.lookup() ++ ).findVarHandle(ItemStack.class, "craftDelegate", ItemStack.class); ++ } catch (final IllegalAccessException | NoSuchFieldException exception) { ++ throw new RuntimeException(exception); ++ } ++ } ++ ++ private static CraftItemStack getCraftStack(final ItemStack bukkit) { ++ if (bukkit instanceof final CraftItemStack craftItemStack) { ++ return craftItemStack; + } else { +- return asNMSCopy(bukkit); ++ return (CraftItemStack) API_ITEM_STACK_CRAFT_DELEGATE_FIELD.get(bukkit); + } + } + ++ @Override ++ public int hashCode() { ++ if (this.handle == null || this.handle.isEmpty()) { ++ return net.minecraft.world.item.ItemStack.EMPTY.hashCode(); ++ } else { ++ int hash = net.minecraft.world.item.ItemStack.hashItemAndComponents(this.handle); ++ hash = hash * 31 + this.handle.getCount(); ++ return hash; ++ } ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (!(obj instanceof final org.bukkit.inventory.ItemStack bukkit)) return false; ++ final CraftItemStack craftStack = getCraftStack(bukkit); ++ if (this.handle == craftStack.handle) return true; ++ else if (this.handle == null || craftStack.handle == null) return false; ++ else if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true; ++ else return net.minecraft.world.item.ItemStack.matches(this.handle, craftStack.handle); ++ } ++ // Paper end ++ ++ // Paper start - MC Utils ++ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) { ++ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack ++ final CraftItemStack craftItemStack = getCraftStack(bukkit); ++ return craftItemStack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : craftItemStack.handle; ++ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack ++ } ++ + public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) { + return old == newInstance ? unwrap(old) : asNMSCopy(newInstance); + } +@@ -47,25 +89,13 @@ public final class CraftItemStack extends ItemStack { + // Paper end - override isEmpty to use vanilla's impl + + public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) { +- if (original instanceof CraftItemStack) { +- CraftItemStack stack = (CraftItemStack) original; +- return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy(); +- } +- if (original == null || original.isEmpty()) { // Paper - override isEmpty to use vanilla's impl; use isEmpty ++ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack ++ if (original == null || original.isEmpty()) { + return net.minecraft.world.item.ItemStack.EMPTY; + } +- +- Item item = CraftItemType.bukkitToMinecraft(original.getType()); +- +- if (item == null) { +- return net.minecraft.world.item.ItemStack.EMPTY; +- } +- +- net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(item, original.getAmount()); +- if (original.hasItemMeta()) { +- CraftItemStack.setItemMeta(stack, original.getItemMeta()); +- } +- return stack; ++ final CraftItemStack stack = getCraftStack(original); ++ return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy(); ++ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack + } + + // Paper start +@@ -88,14 +118,10 @@ public final class CraftItemStack extends ItemStack { + * Copies the NMS stack to return as a strictly-Bukkit stack + */ + public static ItemStack asBukkitCopy(net.minecraft.world.item.ItemStack original) { +- if (original.isEmpty()) { +- return new ItemStack(Material.AIR); +- } +- ItemStack stack = new ItemStack(CraftItemType.minecraftToBukkit(original.getItem()), original.getCount()); +- if (CraftItemStack.hasItemMeta(original)) { +- stack.setItemMeta(CraftItemStack.getItemMeta(original)); +- } +- return stack; ++ // Paper start - no such thing as a "strictly-Bukkit stack" anymore ++ // we copy the stack since it should be a complete copy not a mirror ++ return asCraftMirror(original.copy()); ++ // Paper end + } + + public static CraftItemStack asCraftMirror(net.minecraft.world.item.ItemStack original) { +@@ -313,11 +339,7 @@ public final class CraftItemStack extends ItemStack { + + @Override + public CraftItemStack clone() { +- CraftItemStack itemStack = (CraftItemStack) super.clone(); +- if (this.handle != null) { +- itemStack.handle = this.handle.copy(); +- } +- return itemStack; ++ return new org.bukkit.craftbukkit.inventory.CraftItemStack(this.handle != null ? this.handle.copy() : null); // Paper + } + + @Override +@@ -420,22 +442,14 @@ public final class CraftItemStack extends ItemStack { + if (stack == this) { + return true; + } +- if (!(stack instanceof CraftItemStack)) { +- return stack.getClass() == ItemStack.class && stack.isSimilar(this); +- } +- +- CraftItemStack that = (CraftItemStack) stack; ++ final CraftItemStack that = getCraftStack(stack); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack + if (this.handle == that.handle) { + return true; + } + if (this.handle == null || that.handle == null) { + return false; + } +- Material comparisonType = CraftLegacy.fromLegacy(that.getType()); // This may be called from legacy item stacks, try to get the right material +- if (!(comparisonType == this.getType() && this.getDurability() == that.getDurability())) { +- return false; +- } +- return this.hasItemMeta() ? that.hasItemMeta() && this.handle.getComponents().equals(that.handle.getComponents()) : !that.hasItemMeta(); ++ return net.minecraft.world.item.ItemStack.isSameItemSameComponents(this.handle, that.handle); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +index 9d2feff3df87cfeefe7105d954854af05cef6f69..07539ebfefa3352de5ee7a17f2724cf2c979f399 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +@@ -100,13 +100,14 @@ public class CraftItemType implements ItemType.Typed, Han + @NotNull + @Override + public ItemStack createItemStack(final int amount, @Nullable final Consumer metaConfigurator) { +- final ItemStack itemStack = new ItemStack(this.asMaterial(), amount); ++ // Paper start - re-implement to return CraftItemStack ++ final net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(this.item, amount); ++ final CraftItemStack mirror = CraftItemStack.asCraftMirror(stack); + if (metaConfigurator != null) { +- final ItemMeta itemMeta = itemStack.getItemMeta(); +- metaConfigurator.accept((M) itemMeta); +- itemStack.setItemMeta(itemMeta); ++ mirror.editMeta(this.getItemMetaClass(), metaConfigurator); + } +- return itemStack; ++ return mirror; ++ // Paper start - reimplement to return CraftItemStack + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java +index 9c004e7cb46841d874ab997bf2e3b63ae763aec7..d7c8f26b21276d9ff1d5c7c9738cc1126ce7d4b9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java ++++ b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java +@@ -678,4 +678,16 @@ public class MaterialRerouting { + return itemStack.withType(material); + } + // Paper end - register paper API specific material consumers in rerouting ++ ++ // Paper start - methods added post 1.13, no-op ++ @RerouteStatic("org/bukkit/inventory/ItemStack") ++ public static ItemStack of(final Material material) { ++ return ItemStack.of(material); ++ } ++ ++ @RerouteStatic("org/bukkit/inventory/ItemStack") ++ public static ItemStack of(final Material material, final int amount) { ++ return ItemStack.of(material, amount); ++ } ++ // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae..d70c5546c8bd6f364fad9b24880b6867efdab644 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -700,6 +700,13 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end - hack to get tags for non server-backed registries + ++ // Paper start - proxy ItemStack ++ @Override ++ public org.bukkit.inventory.ItemStack createEmptyStack() { ++ return CraftItemStack.asCraftMirror(null); ++ } ++ // Paper end - proxy ItemStack ++ + /** + * This helper class represents the different NBT Tags. + *

+diff --git a/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e5c2fb160e9d390cdfa0259a3feb9f488b2dc14d +--- /dev/null ++++ b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.configuration; ++ ++import org.bukkit.Material; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertFalse; ++import static org.junit.jupiter.api.Assertions.assertNull; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ ++public abstract class ConfigurationSectionTest extends AbstractTestingBase { ++ public abstract ConfigurationSection getConfigurationSection(); ++ ++ @Test ++ public void testGetItemStack_String() { ++ ConfigurationSection section = getConfigurationSection(); ++ String key = "exists"; ++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); ++ ++ section.set(key, value); ++ ++ assertEquals(value, section.getItemStack(key)); ++ assertNull(section.getString("doesntExist")); ++ } ++ ++ @Test ++ public void testGetItemStack_String_ItemStack() { ++ ConfigurationSection section = getConfigurationSection(); ++ String key = "exists"; ++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); ++ ItemStack def = new ItemStack(Material.STONE, 1); ++ ++ section.set(key, value); ++ ++ assertEquals(value, section.getItemStack(key, def)); ++ assertEquals(def, section.getItemStack("doesntExist", def)); ++ } ++ ++ @Test ++ public void testIsItemStack() { ++ ConfigurationSection section = getConfigurationSection(); ++ String key = "exists"; ++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50); ++ ++ section.set(key, value); ++ ++ assertTrue(section.isItemStack(key)); ++ assertFalse(section.isItemStack("doesntExist")); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..def33c36f207a4c5306b5a895336aa70335c1678 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.configuration; ++ ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.MemoryConfiguration; ++ ++public class MemorySectionTest extends ConfigurationSectionTest { ++ @Override ++ public ConfigurationSection getConfigurationSection() { ++ return new MemoryConfiguration().createSection("section"); ++ } ++} diff --git a/patches/server/1029-Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/server/1029-Make-a-PDC-view-accessible-directly-from-ItemStack.patch new file mode 100644 index 0000000000..95338b76bf --- /dev/null +++ b/patches/server/1029-Make-a-PDC-view-accessible-directly-from-ItemStack.patch @@ -0,0 +1,266 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Wed, 12 Jun 2024 10:29:40 -0700 +Subject: [PATCH] Make a PDC view accessible directly from ItemStack + + +diff --git a/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fac401280d3f3689b00e16c19155ca753faa06e0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java +@@ -0,0 +1,86 @@ ++package io.papermc.paper.persistence; ++ ++import com.google.common.base.Preconditions; ++import java.io.ByteArrayOutputStream; ++import java.io.DataOutputStream; ++import java.io.IOException; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtIo; ++import net.minecraft.nbt.Tag; ++import org.bukkit.NamespacedKey; ++import org.bukkit.craftbukkit.persistence.CraftPersistentDataAdapterContext; ++import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry; ++import org.bukkit.persistence.PersistentDataAdapterContext; ++import org.bukkit.persistence.PersistentDataType; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public abstract class PaperPersistentDataContainerView implements PersistentDataContainerView { ++ ++ protected final CraftPersistentDataTypeRegistry registry; ++ protected final CraftPersistentDataAdapterContext adapterContext; ++ ++ public PaperPersistentDataContainerView(final CraftPersistentDataTypeRegistry registry) { ++ this.registry = registry; ++ this.adapterContext = new CraftPersistentDataAdapterContext(this.registry); ++ } ++ ++ public abstract @Nullable Tag getTag(final String key); ++ ++ public abstract CompoundTag toTagCompound(); ++ ++ @Override ++ public boolean has(final NamespacedKey key, final PersistentDataType type) { ++ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); ++ Preconditions.checkArgument(type != null, "The provided type cannot be null"); ++ ++ final @Nullable Tag value = this.getTag(key.toString()); ++ if (value == null) { ++ return false; ++ } ++ ++ return this.registry.isInstanceOf(type, value); ++ } ++ ++ @Override ++ public boolean has(final NamespacedKey key) { ++ Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper ++ return this.getTag(key.toString()) != null; ++ } ++ ++ @Override ++ public @Nullable C get(final NamespacedKey key, final PersistentDataType type) { ++ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); ++ Preconditions.checkArgument(type != null, "The provided type cannot be null"); ++ ++ final @Nullable Tag value = this.getTag(key.toString()); ++ if (value == null) { ++ return null; ++ } ++ ++ return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext); ++ } ++ ++ @Override ++ public C getOrDefault(final NamespacedKey key, final PersistentDataType type, final C defaultValue) { ++ final C c = this.get(key, type); ++ return c != null ? c : defaultValue; ++ } ++ ++ @Override ++ public PersistentDataAdapterContext getAdapterContext() { ++ return this.adapterContext; ++ } ++ ++ @Override ++ public byte[] serializeToBytes() throws IOException { ++ final net.minecraft.nbt.CompoundTag root = this.toTagCompound(); ++ final ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); ++ try (final DataOutputStream dataOutput = new DataOutputStream(byteArrayOutput)) { ++ NbtIo.write(root, dataOutput); ++ return byteArrayOutput.toByteArray(); ++ } ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index 814e8ece6821e359b504e8c4d140cc38700f2abe..32a41c8b324aad67b9dcf74387aef299e6478a64 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -480,4 +480,63 @@ public final class CraftItemStack extends ItemStack { + return mirrored; + } + // Paper end ++ ++ // Paper start - pdc ++ private net.minecraft.nbt.CompoundTag getPdcTag() { ++ if (this.handle == null) { ++ return new net.minecraft.nbt.CompoundTag(); ++ } ++ final net.minecraft.world.item.component.CustomData customData = this.handle.getOrDefault(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY); ++ // getUnsafe is OK here because we are only ever *reading* the data so immutability is preserved ++ //noinspection deprecation ++ return customData.getUnsafe().getCompound("PublicBukkitValues"); ++ } ++ ++ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); ++ private final io.papermc.paper.persistence.PaperPersistentDataContainerView pdcView = new io.papermc.paper.persistence.PaperPersistentDataContainerView(REGISTRY) { ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag toTagCompound() { ++ return CraftItemStack.this.getPdcTag(); ++ } ++ ++ @Override ++ public net.minecraft.nbt.Tag getTag(final String key) { ++ return CraftItemStack.this.getPdcTag().get(key); ++ } ++ ++ @Override ++ public java.util.Set getKeys() { ++ java.util.Set keys = new java.util.HashSet<>(); ++ CraftItemStack.this.getPdcTag().getAllKeys().forEach(key -> { ++ final String[] keyData = key.split(":", 2); ++ if (keyData.length == 2) { ++ keys.add(new org.bukkit.NamespacedKey(keyData[0], keyData[1])); ++ } ++ }); ++ return java.util.Collections.unmodifiableSet(keys); ++ }; ++ ++ @Override ++ public boolean isEmpty() { ++ return CraftItemStack.this.getPdcTag().isEmpty(); ++ } ++ ++ @Override ++ public void copyTo(final org.bukkit.persistence.PersistentDataContainer other, final boolean replace) { ++ Preconditions.checkArgument(other != null, "The target container cannot be null"); ++ final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer target = (org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer) other; ++ final net.minecraft.nbt.CompoundTag pdcTag = org.bukkit.craftbukkit.inventory.CraftItemStack.this.getPdcTag(); ++ for (final String key : pdcTag.getAllKeys()) { ++ if (replace || !target.getRaw().containsKey(key)) { ++ target.getRaw().put(key, pdcTag.get(key).copy()); ++ } ++ } ++ } ++ }; ++ @Override ++ public io.papermc.paper.persistence.PersistentDataContainerView getPersistentDataContainer() { ++ return this.pdcView; ++ } ++ // Paper end - pdc + } +diff --git a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java +index f55fdd57ced259ad5a95878840e98ffaa3db2e05..9d867f1659433ea15f281c8b441db7e339013100 100644 +--- a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java +@@ -16,11 +16,10 @@ import org.bukkit.persistence.PersistentDataContainer; + import org.bukkit.persistence.PersistentDataType; + import org.jetbrains.annotations.NotNull; + +-public class CraftPersistentDataContainer implements PersistentDataContainer { ++public class CraftPersistentDataContainer extends io.papermc.paper.persistence.PaperPersistentDataContainerView implements PersistentDataContainer { // Paper - split up view and mutable + + private final Map customDataTags = new HashMap<>(); +- private final CraftPersistentDataTypeRegistry registry; +- private final CraftPersistentDataAdapterContext adapterContext; ++ // Paper - move to PersistentDataContainerView + + public CraftPersistentDataContainer(Map customTags, CraftPersistentDataTypeRegistry registry) { + this(registry); +@@ -28,10 +27,15 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { + } + + public CraftPersistentDataContainer(CraftPersistentDataTypeRegistry registry) { +- this.registry = registry; +- this.adapterContext = new CraftPersistentDataAdapterContext(this.registry); ++ super(registry); // Paper - move to PersistentDataContainerView + } + ++ // Paper start ++ @Override ++ public Tag getTag(final String key) { ++ return this.customDataTags.get(key); ++ } ++ // Paper end + + @Override + public void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z value) { +@@ -42,44 +46,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { + this.customDataTags.put(key.toString(), this.registry.wrap(type, type.toPrimitive(value, this.adapterContext))); + } + +- @Override +- public boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { +- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); +- Preconditions.checkArgument(type != null, "The provided type cannot be null"); +- +- Tag value = this.customDataTags.get(key.toString()); +- if (value == null) { +- return false; +- } +- +- return this.registry.isInstanceOf(type, value); +- } +- +- @Override +- public boolean has(NamespacedKey key) { +- Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper +- return this.customDataTags.get(key.toString()) != null; +- } +- +- @Override +- public Z get(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { +- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); +- Preconditions.checkArgument(type != null, "The provided type cannot be null"); +- +- Tag value = this.customDataTags.get(key.toString()); +- if (value == null) { +- return null; +- } +- +- return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext); +- } +- +- @NotNull +- @Override +- public Z getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z defaultValue) { +- Z z = this.get(key, type); +- return z != null ? z : defaultValue; +- } ++ // Paper - move to PersistentDataContainerView + + @NotNull + @Override +@@ -186,16 +153,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { + // Paper end + + // Paper start - byte array serialization +- @Override +- public byte[] serializeToBytes() throws java.io.IOException { +- final net.minecraft.nbt.CompoundTag root = this.toTagCompound(); +- final java.io.ByteArrayOutputStream byteArrayOutput = new java.io.ByteArrayOutputStream(); +- try (final java.io.DataOutputStream dataOutput = new java.io.DataOutputStream(byteArrayOutput)) { +- net.minecraft.nbt.NbtIo.write(root, dataOutput); +- return byteArrayOutput.toByteArray(); +- } +- } +- ++ // Paper - move to PersistentDataContainerView + @Override + public void readFromBytes(final byte[] bytes, final boolean clear) throws java.io.IOException { + if (clear) { diff --git a/patches/server/1030-optimize-dirt-and-snow-spreading.patch b/patches/server/1030-optimize-dirt-and-snow-spreading.patch new file mode 100644 index 0000000000..49de7fcab9 --- /dev/null +++ b/patches/server/1030-optimize-dirt-and-snow-spreading.patch @@ -0,0 +1,78 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lukas81298 +Date: Fri, 22 Jan 2021 21:50:18 +0100 +Subject: [PATCH] optimize dirt and snow spreading + + +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 5a39e8d359dc13383711e49ffb2d1294dad26192..b7165ec19bef1a07f6618fc0429d86cda1b08da4 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +@@ -18,8 +18,13 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + } + + private static boolean canBeGrass(BlockState state, LevelReader world, BlockPos pos) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ return canBeGrass(world.getChunk(pos), state, world, pos); ++ } ++ private static boolean canBeGrass(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { ++ // Paper end - Perf: optimize dirt and snow spreading + BlockPos blockposition1 = pos.above(); +- BlockState iblockdata1 = world.getBlockState(blockposition1); ++ BlockState iblockdata1 = chunk.getBlockState(blockposition1); // Paper - Perf: optimize dirt and snow spreading + + if (iblockdata1.is(Blocks.SNOW) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 1) { + return true; +@@ -36,15 +41,27 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + protected abstract MapCodec codec(); + + private static boolean canPropagate(BlockState state, LevelReader world, BlockPos pos) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ return canPropagate(world.getChunk(pos), state, world, pos); ++ } ++ ++ private static boolean canPropagate(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { ++ // Paper end - Perf: optimize dirt and snow spreading + BlockPos blockposition1 = pos.above(); + +- return SpreadingSnowyDirtBlock.canBeGrass(state, world, pos) && !world.getFluidState(blockposition1).is(FluidTags.WATER); ++ return SpreadingSnowyDirtBlock.canBeGrass(chunk, state, world, pos) && !chunk.getFluidState(blockposition1).is(FluidTags.WATER); // Paper - Perf: optimize dirt and snow spreading + } + + @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)) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); ++ if (cachedBlockChunk == null) { // Is this needed? ++ return; ++ } ++ if (!SpreadingSnowyDirtBlock.canBeGrass(cachedBlockChunk, state, world, pos)) { ++ // Paper end - Perf: optimize dirt and snow spreading + // CraftBukkit start + if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { + return; +@@ -57,9 +74,19 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + + for (int i = 0; i < 4; ++i) { + BlockPos blockposition1 = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1); +- +- if (world.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(iblockdata1, world, blockposition1)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, world.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit ++ // Paper start - Perf: optimize dirt and snow spreading ++ if (pos.getX() == blockposition1.getX() && pos.getY() == blockposition1.getY() && pos.getZ() == blockposition1.getZ()) { ++ continue; ++ } ++ net.minecraft.world.level.chunk.ChunkAccess access; ++ if (cachedBlockChunk.locX == blockposition1.getX() >> 4 && cachedBlockChunk.locZ == blockposition1.getZ() >> 4) { ++ access = cachedBlockChunk; ++ } else { ++ access = world.getChunkAt(blockposition1); ++ } ++ if (access.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(access, iblockdata1, world, blockposition1)) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, access.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit ++ // Paper end - Perf: optimize dirt and snow spreading + } + } + } diff --git a/patches/server/1031-Fix-NPE-for-Jukebox-setRecord.patch b/patches/server/1031-Fix-NPE-for-Jukebox-setRecord.patch new file mode 100644 index 0000000000..e15a77ee75 --- /dev/null +++ b/patches/server/1031-Fix-NPE-for-Jukebox-setRecord.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Mon, 17 Jun 2024 17:41:09 -0700 +Subject: [PATCH] Fix NPE for Jukebox#setRecord + +Fallback to the global registry if no level exists + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java +index 1497e76b548ad76b5aaa297bdd35723e6a8f1f8d..c5069730b25e6f0dfb4e5db3271c91116b485f58 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java +@@ -200,7 +200,7 @@ public class JukeboxBlockEntity extends BlockEntity implements Clearable, Contai + public void setSongItemWithoutPlaying(ItemStack itemstack, long ticksSinceSongStarted) { // CraftBukkit - add argument + this.item = itemstack; + this.jukeboxSongPlayer.song = null; // CraftBukkit - reset +- JukeboxSong.fromStack(this.level.registryAccess(), itemstack).ifPresent((holder) -> { ++ JukeboxSong.fromStack(this.level != null ? this.level.registryAccess() : org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry(), itemstack).ifPresent((holder) -> { // Paper - fallback to other RegistyrAccess if no level + this.jukeboxSongPlayer.setSongWithoutPlaying(holder, ticksSinceSongStarted); // CraftBukkit - add argument + }); + // CraftBukkit start - add null check for level diff --git a/patches/server/1032-Fix-CraftWorld-isChunkGenerated.patch b/patches/server/1032-Fix-CraftWorld-isChunkGenerated.patch new file mode 100644 index 0000000000..f91e8b2bd0 --- /dev/null +++ b/patches/server/1032-Fix-CraftWorld-isChunkGenerated.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Tue, 18 Jun 2024 12:43:06 -0700 +Subject: [PATCH] Fix CraftWorld#isChunkGenerated + +The upstream implementation is returning true for non-full chunks. + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index fe9e8d1d4ae1f7a4e8f4cf5688004fc969422b3c..8045d6c9398d1c88595da6e41aa1ed27fb6fbad0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -367,11 +367,28 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean isChunkGenerated(int x, int z) { +- try { +- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); +- } catch (InterruptedException | ExecutionException ex) { +- throw new RuntimeException(ex); ++ // Paper start - Fix this method ++ if (!Bukkit.isPrimaryThread()) { ++ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { ++ return CraftWorld.this.isChunkGenerated(x, z); ++ }, world.getChunkSource().mainThreadProcessor).join(); ++ } ++ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); ++ if (chunk != null) { ++ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; + } ++ final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad( ++ this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, future::complete ++ ); ++ world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone); ++ return future.thenApply(c -> { ++ if (c != null) { ++ return c.getPersistedStatus() == ChunkStatus.FULL; ++ } ++ return false; ++ }).join(); ++ // Paper end - Fix this method + } + + @Override diff --git a/patches/server/1033-Add-debug-for-chunk-system-unload-crash.patch b/patches/server/1033-Add-debug-for-chunk-system-unload-crash.patch new file mode 100644 index 0000000000..401b927f05 --- /dev/null +++ b/patches/server/1033-Add-debug-for-chunk-system-unload-crash.patch @@ -0,0 +1,57 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Wed, 19 Jun 2024 10:52:07 -0700 +Subject: [PATCH] Add debug for chunk system unload crash + +Somehow, a chunkholder is present in the unload queue after +it has been unloaded. It is likely that this is a result of +adding the chunk holder to the unload queue while it is +unloading. However, that should not be possible. + +To find out where it is being added to the unload queue, track +the last stacktrace which adds to the unload queue and check +on chunk holder remove if the holder is present in the unload queue +and log the stacktrace. + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +index d5fc5756ea960096ff23376a6b7ac68a2a462d22..0715514c8bb49c805b04f42ff37903a7d590aaa8 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -748,9 +748,22 @@ public final class NewChunkHolder { + + /** Unloaded from chunk map */ + private boolean unloaded; ++ private Throwable lastUnloadAdd; + + void markUnloaded() { + this.unloaded = true; ++ if (this.inUnloadQueue) { ++ if (this.lastUnloadAdd != null) { ++ LOGGER.error("Unloaded chunkholder " + this.toString() + " while in the unload queue", this.lastUnloadAdd); ++ } else { ++ // should never happen ++ LOGGER.error("Unloaded chunkholder " + this.toString() + " while in the unload queue without a throwable"); ++ } ++ ++ // prevent crash by removing (note: we hold scheduling lock here) ++ this.inUnloadQueue = false; ++ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); ++ } + } + + private boolean inUnloadQueue = false; +@@ -768,12 +781,14 @@ public final class NewChunkHolder { + // ensure in unload queue + if (!this.inUnloadQueue) { + this.inUnloadQueue = true; ++ this.lastUnloadAdd = new Throwable(); + this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); + } + } else { + // ensure not in unload queue + if (this.inUnloadQueue) { + this.inUnloadQueue = false; ++ this.lastUnloadAdd = null; + this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); + } + } diff --git a/patches/unapplied/server/1001-Fix-World-isChunkGenerated-calls.patch b/patches/unapplied/server/1001-Fix-World-isChunkGenerated-calls.patch deleted file mode 100644 index 37c3afba66..0000000000 --- a/patches/unapplied/server/1001-Fix-World-isChunkGenerated-calls.patch +++ /dev/null @@ -1,243 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 15 Jun 2019 08:54:33 -0700 -Subject: [PATCH] Fix World#isChunkGenerated calls - -Optimize World#loadChunk() too -This patch also adds a chunk status cache on region files (note that -its only purpose is to cache the status on DISK) - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5..1d2d4d38ff3414896d07f7f4e40d0edb9a8c3993 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -735,9 +735,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - - private CompletableFuture> readChunk(ChunkPos chunkPos) { -- return this.read(chunkPos).thenApplyAsync((optional) -> { -- return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit -- }, Util.backgroundExecutor()); -+ // Paper start - Cache chunk status on disk -+ try { -+ return CompletableFuture.completedFuture(Optional.ofNullable(this.readConvertChunkSync(chunkPos))); -+ } catch (Throwable thr) { -+ return CompletableFuture.failedFuture(thr); -+ } -+ // Paper end - Cache chunk status on disk - } - - // CraftBukkit start -@@ -746,6 +750,60 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // CraftBukkit end - } - -+ // Paper start - Cache chunk status on disk -+ @Nullable -+ public CompoundTag readConvertChunkSync(ChunkPos pos) throws IOException { -+ CompoundTag nbttagcompound = this.readSync(pos); -+ if (nbttagcompound == null) { -+ return null; -+ } -+ -+ nbttagcompound = this.upgradeChunkTag(nbttagcompound, pos); // CraftBukkit -+ if (nbttagcompound == null) { -+ return null; -+ } -+ -+ this.updateChunkStatusOnDisk(pos, nbttagcompound); -+ -+ return nbttagcompound; -+ } -+ -+ public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); -+ -+ return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ } -+ -+ public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true); -+ -+ if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) { -+ return null; -+ } -+ -+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ -+ if (status != null) { -+ return status; -+ } -+ -+ this.readChunk(chunkPos); -+ -+ return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ } -+ -+ public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false); -+ -+ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound)); -+ } -+ -+ public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { -+ ChunkHolder chunkHolder = io.papermc.paper.chunk.system.ChunkSystem.getUnloadingChunkHolder(this.level, chunkX, chunkZ); -+ return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); -+ } -+ // Paper end - Cache chunk status on disk -+ - public boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) { // Paper - public - // Spigot start - return this.anyPlayerCloseEnoughForSpawning(pos, false); -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 f994f91dd4b4a0a6d540a5605af0b6623e229f84..cf43daa019b239464401784938d01af83f9da47c 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 -@@ -52,6 +52,29 @@ public class RegionFile implements AutoCloseable { - @VisibleForTesting - protected final RegionBitmap usedSectors; - public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper -+ // Paper start - Cache chunk status -+ private final net.minecraft.world.level.chunk.status.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.status.ChunkStatus[32 * 32]; -+ -+ private boolean closed; -+ -+ // invoked on write/read -+ public void setStatus(int x, int z, net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ if (this.closed) { -+ // We've used an invalid region file. -+ throw new IllegalStateException("RegionFile is closed"); -+ } -+ this.statuses[getChunkLocation(x, z)] = status; -+ } -+ -+ public net.minecraft.world.level.chunk.status.ChunkStatus getStatusIfCached(int x, int z) { -+ if (this.closed) { -+ // We've used an invalid region file. -+ throw new IllegalStateException("RegionFile is closed"); -+ } -+ final int location = getChunkLocation(x, z); -+ return this.statuses[location]; -+ } -+ // Paper end - Cache chunk status - - public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { - this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format -@@ -417,6 +440,7 @@ public class RegionFile implements AutoCloseable { - return this.getOffset(pos) != 0; - } - -+ private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - Cache chunk status; OBFHELPER - sort of, mirror of logic below - private static int getOffsetIndex(ChunkPos pos) { - return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32; - } -@@ -427,6 +451,7 @@ public class RegionFile implements AutoCloseable { - synchronized (this) { - try { - // Paper end -+ this.closed = true; // Paper - Cache chunk status - try { - this.padToFullSector(); - } finally { -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 c2838ae91b7f078369b63503df57a1eb5d2b0df5..c33640859aab837c85f3e860fe2241a0e78bb09a 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 -@@ -268,6 +268,7 @@ public class RegionFileStorage implements AutoCloseable { - - try { - NbtIo.write(nbt, (DataOutput) dataoutputstream); -+ regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - Cache chunk status - regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone - // Paper start - don't write garbage data to disk if writing serialization fails - dataoutputstream.close(); // Only write if successful -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 9c06c3729b09726e1da6ff8fb975cef2aeba9643..8f50d893f8f9dea306756b640abd2373ee028a86 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -391,9 +391,23 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public boolean isChunkGenerated(int x, int z) { -+ // Paper start - Fix this method -+ if (!Bukkit.isPrimaryThread()) { -+ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ return CraftWorld.this.isChunkGenerated(x, z); -+ }, world.getChunkSource().mainThreadProcessor).join(); -+ } -+ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); -+ if (chunk == null) { -+ chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); -+ } -+ if (chunk != null) { -+ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; -+ } - try { -- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); -- } catch (InterruptedException | ExecutionException ex) { -+ return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL; -+ } catch (java.io.IOException ex) { -+ // Paper end - Fix this method - throw new RuntimeException(ex); - } - } -@@ -572,20 +586,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { - public boolean loadChunk(int x, int z, boolean generate) { - org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot - warnUnsafeChunk("loading a faraway chunk", x, z); // Paper -- ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper -- -- // If generate = false, but the chunk already exists, we will get this back. -- if (chunk instanceof ImposterProtoChunk) { -- // We then cycle through again to get the full chunk immediately, rather than after the ticket addition -- chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); -- } -- -- if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) { -- this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper -+ // Paper start - Optimize this method -+ ChunkPos chunkPos = new ChunkPos(x, z); -+ ChunkAccess immediate = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); -+ if (immediate != null) { -+ // Plugins should use plugin tickets instead of this method to keep a chunk perpetually loaded - return true; - } - -- return false; -+ if (!generate) { -+ immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); -+ if (immediate != null) { -+ if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) { -+ return false; // not full status -+ } -+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper -+ world.getChunk(x, z); // make sure we're at ticket level 32 or lower -+ return true; -+ } -+ net.minecraft.world.level.chunk.storage.RegionFile file; -+ try { -+ file = world.getChunkSource().chunkMap.regionFileCache.getRegionFile(chunkPos, false); -+ } catch (java.io.IOException ex) { -+ throw new RuntimeException(ex); -+ } -+ -+ ChunkStatus status = file.getStatusIfCached(x, z); -+ if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) { -+ return false; -+ } -+ -+ ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true); -+ if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) { -+ return false; -+ } -+ -+ // fall through to load -+ // we do this so we do not re-read the chunk data on disk -+ } -+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper -+ world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); -+ return true; -+ // Paper end - Optimize this method - } - - @Override diff --git a/patches/unapplied/server/1007-Optimize-Collision-to-not-load-chunks.patch b/patches/unapplied/server/1007-Optimize-Collision-to-not-load-chunks.patch deleted file mode 100644 index f089edd842..0000000000 --- a/patches/unapplied/server/1007-Optimize-Collision-to-not-load-chunks.patch +++ /dev/null @@ -1,109 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 2 Apr 2020 02:37:57 -0400 -Subject: [PATCH] Optimize Collision to not load chunks - -The collision code takes an AABB and generates a cuboid of checks rather -than a cylinder, so at high velocity this can generate a lot of chunk checks. - -Treat an unloaded chunk as a collision for entities, and also for players if -the "prevent moving into unloaded chunks" setting is enabled. - -If that serting is not enabled, collisions will be ignored for players, since -movement will load only the chunk the player enters anyways and avoids loading -massive amounts of surrounding chunks due to large AABB lookups. - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 8f1a2de2a5542929f8a2fdd4d38de141eda985b2..29b35b8c0a6001d626e327a82eaff26d068660db 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -934,6 +934,7 @@ public abstract class PlayerList { - entityplayer1.setShiftKeyDown(false); - entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - -+ worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper - while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { - // CraftBukkit end - entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 6f48af629392f4426e50f752424fc9f58985688a..26b21af65ff63c83fb3be80740e41b40f0feb944 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -244,6 +244,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - // Paper end - Share random for entities to make them more random - public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason - -+ public boolean collisionLoadChunks = false; // Paper - private CraftEntity bukkitEntity; - - public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper -diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java -index 1c10835b59aaefa3a65ff64f784620bdc54ddcdc..cd89623a44f02d7db77f0d0f87545cf80841f403 100644 ---- a/src/main/java/net/minecraft/world/level/BlockCollisions.java -+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java -@@ -66,18 +66,37 @@ public class BlockCollisions extends AbstractIterator { - @Override - protected T computeNext() { - while (this.cursor.advance()) { -- int i = this.cursor.nextX(); -- int j = this.cursor.nextY(); -- int k = this.cursor.nextZ(); -+ int i = this.cursor.nextX(); final int x = i; // Paper - OBFHELPER -+ int j = this.cursor.nextY(); final int y = j; // Paper - OBFHELPER -+ int k = this.cursor.nextZ(); final int z = k; // Paper - OBFHELPER - int l = this.cursor.getNextType(); - if (l != 3) { -- BlockGetter blockGetter = this.getChunk(i, k); -- if (blockGetter != null) { -- this.pos.set(i, j, k); -- BlockState blockState = blockGetter.getBlockState(this.pos); -- if ((!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) -- && (l != 1 || blockState.hasLargeCollisionShape()) -- && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { -+ // Paper start - ensure we don't load chunks -+ // BlockGetter blockGetter = this.getChunk(i, k); -+ if (true) { -+ final @Nullable Entity source = this.context instanceof net.minecraft.world.phys.shapes.EntityCollisionContext entityContext ? entityContext.getEntity() : null; -+ boolean far = source != null && io.papermc.paper.util.MCUtil.distanceSq(source.getX(), y, source.getZ(), x, y, z) > 14; -+ this.pos.set(x, y, z); -+ BlockState blockState; -+ if (this.collisionGetter instanceof net.minecraft.server.level.WorldGenRegion) { -+ BlockGetter blockGetter = this.getChunk(x, z); -+ if (blockGetter == null) { -+ continue; -+ } -+ blockState = blockGetter.getBlockState(this.pos); -+ } else if ((!far && source instanceof net.minecraft.server.level.ServerPlayer) || (source != null && source.collisionLoadChunks)) { -+ blockState = this.collisionGetter.getBlockState(this.pos); -+ } else { -+ blockState = this.collisionGetter.getBlockStateIfLoaded(this.pos); -+ } -+ if (blockState == null) { -+ if (!(source instanceof net.minecraft.server.level.ServerPlayer) || source.level().paperConfig().chunks.preventMovingIntoUnloadedChunks) { -+ return this.resultProvider.apply(new BlockPos.MutableBlockPos(x, y, z), Shapes.create(far ? source.getBoundingBox() : new AABB(new BlockPos(x, y, z)))); -+ } -+ continue; -+ } -+ if (/*(!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) &&*/ (l != 1 || blockState.hasLargeCollisionShape()) && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { // Paper - onlySuffocatingBlocks is only true on the client, so we don't care about it here -+ // Paper end - VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); - if (voxelShape == Shapes.block()) { - if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { -diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java -index e57cb7fe53e915d24246e44c7f49971f5b2ab2cf..1ad0c976c6e2d6d31397dff850a9de7c16d16fba 100644 ---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java -+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java -@@ -44,11 +44,13 @@ public interface CollisionGetter extends BlockGetter { - } - - default boolean noCollision(@Nullable Entity entity, AABB box) { -+ try { if (entity != null) entity.collisionLoadChunks = true; // Paper - for (VoxelShape voxelShape : this.getBlockCollisions(entity, box)) { - if (!voxelShape.isEmpty()) { - return false; - } - } -+ } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper - - if (!this.getEntityCollisions(entity, box).isEmpty()) { - return false; diff --git a/patches/unapplied/server/1017-Improve-boat-collision-performance.patch b/patches/unapplied/server/1017-Improve-boat-collision-performance.patch deleted file mode 100644 index ee5adde385..0000000000 --- a/patches/unapplied/server/1017-Improve-boat-collision-performance.patch +++ /dev/null @@ -1,68 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 2 Aug 2021 10:10:40 +0200 -Subject: [PATCH] Improve boat collision performance - - -diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java -index 2cd0a4dc4f0baa08bd7f5a053303bb63733f0bab..0bd367235f80c1f0d319a6aa5130d82ad82d895c 100644 ---- a/src/main/java/net/minecraft/Util.java -+++ b/src/main/java/net/minecraft/Util.java -@@ -125,6 +125,7 @@ public class Util { - .filter(fileSystemProvider -> fileSystemProvider.getScheme().equalsIgnoreCase("jar")) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No jar file system provider found")); -+ public static final double COLLISION_EPSILON = 1.0E-7; // Paper - Improve boat collision performance - private static Consumer thePauser = message -> { - }; - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 56351d5c2b4ea2c1b335253153cedafdf058d9ae..9601d3def5e4ac0650bae42a1da2a64ab1e5aef7 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1459,7 +1459,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - if (!source.is(DamageTypeTags.IS_PROJECTILE)) { - Entity entity = source.getDirectEntity(); - -- if (entity instanceof LivingEntity) { -+ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance - LivingEntity entityliving = (LivingEntity) entity; - - this.blockUsingShield(entityliving); -@@ -1553,11 +1553,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - if (entity1 != null && !source.is(DamageTypeTags.NO_KNOCKBACK)) { -- double d0 = entity1.getX() - this.getX(); -+ final boolean far = entity1.distanceToSqr(this) > (200.0 * 200.0); // Paper - Improve boat collision performance -+ double d0 = far ? (Math.random() - Math.random()) : entity1.getX() - this.getX(); // Paper - Improve boat collision performance - - double d1; - -- for (d1 = entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { -+ for (d1 = far ? Math.random() - Math.random() : entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { // Paper - Improve boat collision performance - d0 = (Math.random() - Math.random()) * 0.01D; - } - -@@ -2324,7 +2325,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.hurtCurrentlyUsedShield((float) -event.getDamage(DamageModifier.BLOCKING)); - Entity entity = damagesource.getDirectEntity(); - -- if (entity instanceof LivingEntity) { -+ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance - this.blockUsingShield((LivingEntity) entity); - } - } -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -index 549202706396fee91c23f6e34f7faa7672e4b03f..b068cff9b5aa457d65b679529956e8210296d799 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -@@ -686,7 +686,7 @@ public class Boat extends VehicleEntity implements VariantHolder { - this.invFriction = 0.05F; - if (this.oldStatus == Boat.Status.IN_AIR && this.status != Boat.Status.IN_AIR && this.status != Boat.Status.ON_LAND) { - this.waterLevel = this.getY(1.0D); -- this.setPos(this.getX(), (double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D, this.getZ()); -+ this.move(MoverType.SELF, new Vec3(0.0, ((double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D) - this.getY(), 0.0)); // Paper - Improve boat collision performance - this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D)); - this.lastYd = 0.0D; - this.status = Boat.Status.IN_WATER; diff --git a/patches/unapplied/server/1018-Optimise-general-POI-access.patch b/patches/unapplied/server/1018-Optimise-general-POI-access.patch deleted file mode 100644 index 118734c246..0000000000 --- a/patches/unapplied/server/1018-Optimise-general-POI-access.patch +++ /dev/null @@ -1,1060 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 31 Jan 2021 02:29:24 -0800 -Subject: [PATCH] Optimise general POI access - -There are a couple of problems with mojang's POI code. -Firstly, it's all streams. Unsurprisingly, stacking -streams on top of each other is horrible for performance -and ultimately took up half of a villager's tick! - -Secondly, sometime's the search radius is large and there are -a significant number of poi entries per chunk section. Even -removing streams at this point doesn't help much. The only solution -is to start at the search point and iterate outwards. This -type of approach shows massive gains for portals, simply because -we can avoid sync loading a large area of chunks. I also tested -a massive farm I found in JellySquid's discord, which showed -to benefit significantly simply because the farm had so many -portal blocks that searching through them all was very slow. - -Great care has been taken so that behavior remains identical to -vanilla, however I cannot account for oddball Stream API -implementations, if they even exist (streams can technically -be loose with iteration order in a sorted stream given its -source stream is not tagged with ordered, and mojang does not -tag the source stream as ordered). However in my testing on openjdk -there showed no difference, as expected. - -This patch also specifically optimises other areas of code to -use PoiAccess. For example, some villager AI and portaling code -had to be specifically modified. - -diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java -new file mode 100644 -index 0000000000000000000000000000000000000000..69be1761b3b5ba7b496c1c10a4db897e6212d671 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/PoiAccess.java -@@ -0,0 +1,804 @@ -+package io.papermc.paper.util; -+ -+import com.mojang.datafixers.util.Pair; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; -+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import java.util.function.BiPredicate; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Holder; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.ai.village.poi.PoiManager; -+import net.minecraft.world.entity.ai.village.poi.PoiRecord; -+import net.minecraft.world.entity.ai.village.poi.PoiSection; -+import net.minecraft.world.entity.ai.village.poi.PoiType; -+import java.util.ArrayList; -+import java.util.HashSet; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+import java.util.Set; -+import java.util.function.Predicate; -+ -+/** -+ * Provides optimised access to POI data. All returned values will be identical to vanilla. -+ */ -+public final class PoiAccess { -+ -+ protected static double clamp(final double val, final double min, final double max) { -+ return (val < min ? min : (val > max ? max : val)); -+ } -+ -+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, -+ final double boxMaxX, final double boxMaxY, final double boxMaxZ, -+ -+ final double circleX, final double circleY, final double circleZ) { -+ // is the circle center inside the box? -+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { -+ return 0.0; -+ } -+ -+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; -+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; -+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; -+ -+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; -+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; -+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; -+ -+ double centerDiffX = circleX - boxCenterX; -+ double centerDiffY = circleY - boxCenterY; -+ double centerDiffZ = circleZ - boxCenterZ; -+ -+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); -+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); -+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); -+ -+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); -+ } -+ -+ -+ // key is: -+ // upper 32 bits: -+ // upper 16 bits: max y section -+ // lower 16 bits: min y section -+ // lower 32 bits: -+ // upper 16 bits: section -+ // lower 16 bits: radius -+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { -+ return ( -+ (maxSection & 0xFFFFL) << (64 - 16) -+ | (minSection & 0xFFFFL) << (64 - 32) -+ | (section & 0xFFFFL) << (64 - 48) -+ | (radius & 0xFFFFL) << (64 - 64) -+ ); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final Set ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(record.getPos()); -+ } -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; -+ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); -+ } -+ -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final List closestRecords = new ArrayList<>(); -+ double closestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.world); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.world); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() -+ ); -+ if (sectionDistanceSquared > closestDistanceSquared) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > closestDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange < closestDistanceSquared) { -+ closestRecords.clear(); -+ closestDistanceSquared = dataRange; -+ } -+ closestRecords.add(poiData); -+ } -+ } -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecords.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecords); -+ } -+ -+ // finds the closest poi entry pos. -+ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findNearestPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // finds the closest `max` poi entry positions. -+ public static void findNearestPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ // finds the closest poi entry. -+ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, -+ 1, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // finds the closest `max` poi entries. -+ public static void findNearestPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); -+ int totalRecords = 0; -+ double furthestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.world); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.world); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() -+ ); -+ -+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > maxDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared && totalRecords >= max) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared) { -+ // we know totalRecords < max, so this entry is now our furthest -+ furthestDistanceSquared = dataRange; -+ } -+ -+ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { -+ return new ArrayList<>(); -+ }).add(poiData); -+ -+ if (++totalRecords >= max) { -+ if (closestRecords.size() >= 2) { -+ int entriesInClosest = 0; -+ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); -+ double nextFurthestDistanceSquared = 0.0; -+ -+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ entriesInClosest += recordEntry.getValue().size(); -+ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); -+ } -+ -+ if (entriesInClosest >= max) { -+ // the last set of entries at range wont even be considered for sure... nuke em -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ totalRecords -= recordEntry.getValue().size(); -+ iterator.remove(); -+ -+ furthestDistanceSquared = nextFurthestDistanceSquared; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ final List closestRecordsUnsorted = new ArrayList<>(); -+ -+ // we're done here, so now just flatten the map and sort it. -+ -+ for (final List records : closestRecords.values()) { -+ closestRecordsUnsorted.addAll(records); -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecordsUnsorted.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller section y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // trim out any entries exceeding our maximum -+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { -+ closestRecordsUnsorted.remove(i); -+ } -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecordsUnsorted); -+ } -+ -+ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findAnyPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ public static void findAnyPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findAnyPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ public static void findAnyPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List ret) { -+ // the biggest issue with the original mojang implementation is that they chain so many streams together -+ // the amount of streams chained just rolls performance, even if nothing is iterated over -+ final Predicate occupancyFilter = occupancy.getTest(); -+ final double rangeSquared = range * range; -+ -+ int added = 0; -+ -+ // First up, we need to iterate the chunks -+ // all the values here are in chunk sections -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ // Vanilla iterates by x until max is reached then increases z -+ // vanilla also searches by increasing Y section value -+ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { -+ for (int currX = lowerX; currX <= upperX; ++currX) { -+ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : -+ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); -+ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); -+ if (poiSection == null) { -+ continue; -+ } -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ // found one! -+ ret.add(poiData); -+ if (++added >= max) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ private PoiAccess() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -index e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -@@ -71,11 +71,11 @@ public class AcquirePoi { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( -- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE -- ) -- .limit(5L) -- .collect(Collectors.toSet()); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); -+ Set, BlockPos>> set = new java.util.HashSet<>(poiposes); -+ // Paper end - optimise POI access - Path path = findPathToPois(entity, set); - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllWithType( -- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY -- ) -- .collect(Collectors.toSet()); -- Path path = AcquirePoi.findPathToPois(entity, set); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ // don't ask me why it's unbounded. ask mojang. -+ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); -+ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); -+ // Paper end - optimise POI access - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); - Optional> optional = poiManager.getType(blockPos); -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 7b52b0507cbda76aee1db954641f397bef51f94d..c6f193339fdcbcc938d4eafdcad0b112cf1698d5 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -@@ -138,36 +138,45 @@ public class PoiManager extends SectionStorage { - public Optional find( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - } - - public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - re-route to faster logic - } - - public Optional, BlockPos>> findClosestWithType( - Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) -- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); -+ // Paper start - re-route to faster logic -+ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( -+ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false -+ )); -+ // Paper end - re-route to faster logic - } - - public Optional findClosest( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .filter(posPredicate) -- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - re-route to faster logic - } - - public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> biPredicate, BlockPos pos, int radius) { -- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) -- .filter(poi -> biPredicate.test(poi.getPoiType(), poi.getPos())) -- .findFirst() -+ // Paper start - re-route to faster logic -+ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( -+ this, typePredicate, biPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false -+ ); -+ return Optional.ofNullable(closest) -+ // Paper end - re-route to faster logic - .map(poi -> { - poi.acquireTicket(); - return poi.getPos(); -@@ -182,8 +191,21 @@ public class PoiManager extends SectionStorage { - int radius, - RandomSource random - ) { -- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); -- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); -+ // Paper start - re-route to faster logic -+ List list = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( -+ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list -+ ); -+ -+ // the old method shuffled the list and then tried to find the first element in it that -+ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a -+ // shuffle entirely, and just pick a random element from list -+ if (list.isEmpty()) { -+ return Optional.empty(); -+ } -+ -+ return Optional.of(list.get(random.nextInt(list.size())).getPos()); -+ // Paper end - re-route to faster logic - } - - public boolean release(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -index 5b7deae326228e482b218aeebd857a59b7434eaf..4ee7d75c56d9f9ff3607276857dde84410ba3f2a 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -@@ -26,7 +26,7 @@ import org.slf4j.Logger; - public class PoiSection { - private static final Logger LOGGER = LogUtils.getLogger(); - private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); -- private final Map, Set> byType = Maps.newHashMap(); -+ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor - private final Runnable setDirty; - private boolean isValid; - public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -index 883fbe5c81e3be27007a1a0489f80ba1863e5a04..a4a919d8373f1535e336de7e648d41a07efb1cba 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -@@ -74,11 +74,11 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl - } - - @Nullable -- protected Optional get(long pos) { -+ public Optional get(long pos) { // Paper - public - return this.storage.get(pos); - } - -- protected Optional getOrLoad(long pos) { -+ public Optional getOrLoad(long pos) { // Paper - public - if (this.outsideStoredRange(pos)) { - return Optional.empty(); - } else { -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 a61959700d5e00739a79eaa617ac383160335f26..2407d93c11b806701fc7d192f39d535128281e80 100644 ---- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -@@ -51,18 +51,39 @@ public class PortalForcer { - // int i = flag ? 16 : 128; - // CraftBukkit end - -- villageplace.ensureLoadedAndValid(this.level, blockposition, i); -- Optional optional = villageplace.getInSquare((holder) -> { -- return holder.is(PoiTypes.NETHER_PORTAL); -- }, blockposition, i, PoiManager.Occupancy.ANY).filter((villageplacerecord) -> { -- return worldborder.isWithinBounds(villageplacerecord.getPos()) && !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> villageplacerecord.getPos().getY() >= v)); // Paper - Configurable nether ceiling damage -- }).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error -- return villageplacerecord.getPos().distSqr(blockposition); -- }).thenComparingInt((villageplacerecord) -> { -- return villageplacerecord.getPos().getY(); -- })).filter((villageplacerecord) -> { -- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -- }).findFirst(); -+ // Paper start - optimise portals -+ Optional optional; -+ java.util.List records = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( -+ villageplace, -+ type -> type.is(PoiTypes.NETHER_PORTAL), -+ (BlockPos pos) -> { -+ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); -+ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) -+ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { -+ // why would we generate the chunk? -+ return false; -+ } -+ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage -+ return false; -+ } -+ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -+ }, -+ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records -+ ); -+ -+ // this gets us most of the way there, but we bias towards lower y values. -+ PoiRecord lowestYRecord = null; -+ for (PoiRecord record : records) { -+ if (lowestYRecord == null) { -+ lowestYRecord = record; -+ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { -+ lowestYRecord = record; -+ } -+ } -+ // now we're done -+ optional = Optional.ofNullable(lowestYRecord); -+ // Paper end - optimise portals - - return optional.map((villageplacerecord) -> { - BlockPos blockposition1 = villageplacerecord.getPos(); diff --git a/patches/unapplied/server/1020-Execute-chunk-tasks-mid-tick.patch b/patches/unapplied/server/1020-Execute-chunk-tasks-mid-tick.patch deleted file mode 100644 index aecd8e5402..0000000000 --- a/patches/unapplied/server/1020-Execute-chunk-tasks-mid-tick.patch +++ /dev/null @@ -1,176 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 6 Apr 2020 04:20:44 -0700 -Subject: [PATCH] Execute chunk tasks mid-tick - -This will help the server load chunks if tick times are high. - -diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java -index 6b3cde6d4d1e63bec01f502f2027ee9fddac08aa..46449728f69ee7d4f78470f8da23c055acd53a3b 100644 ---- a/src/main/java/co/aikar/timings/MinecraftTimings.java -+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java -@@ -48,6 +48,8 @@ public final class MinecraftTimings { - public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); - public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Paper - add timings for scoreboard search - -+ public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks"); -+ - private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap(); - - private MinecraftTimings() {} -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 03552ca486807d057147f6a05f024989d593d046..64ae2745c3144d43cad12dfe9baf1a291756d129 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1411,8 +1411,79 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { -+ if (!moreTasks) { -+ lastMidTickExecuteFailure = currTime; -+ } -+ -+ // note: negative values reduce the time -+ long overuse = diff - MAX_CHUNK_EXEC_TIME; -+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms -+ // make sure something like a GC or dumb plugin doesn't screw us over... -+ overuse = 10L * 1000L * 1000L; // 10ms -+ } -+ -+ double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; -+ long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); -+ -+ lastMidTickExecute = currTime + extraSleep; -+ return; -+ } -+ } -+ } finally { -+ co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming(); -+ } -+ } -+ // Paper end - execute chunk tasks mid tick -+ - private boolean pollTaskInternal() { - if (super.pollTask()) { -+ this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 6b258a2bd85bb71b030d42b488e1a4742d1e147d..01577dbb16ff6abc9edcf897f7fadaad14350184 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -559,6 +559,7 @@ public class ServerChunkCache extends ChunkSource { - boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit - Iterator iterator1 = list.iterator(); - -+ int chunksTicked = 0; // Paper - while (iterator1.hasNext()) { - ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next(); - LevelChunk chunk1 = chunkproviderserver_a.chunk; -@@ -572,6 +573,7 @@ public class ServerChunkCache extends ChunkSource { - - if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { - this.level.tickChunk(chunk1, l); -+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper - } - } - } -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 1ec7865e2e2bd23607e9b3041d77bd4badf39a4a..3a49f8933bc6ca1862994b7e0b3006f5236dd94a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -221,6 +221,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - private final StructureCheck structureCheck; - private final boolean tickTime; - private final RandomSequences randomSequences; -+ public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick - - // CraftBukkit start - public final LevelStorageSource.LevelStorageAccess convertable; -@@ -1211,6 +1212,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (fluid1.is(fluid)) { - fluid1.tick(this, pos); - } -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick - - } - -@@ -1220,6 +1222,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (iblockdata.is(block)) { - iblockdata.tick(this, pos, this.random); - } -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick - - } - -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index d3d7abb2d31e8ce9f9c53eca66a83a1c28fec792..dab55ab08665f2b5ae0c899a4ab07c18460552ae 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -915,6 +915,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // Spigot end - } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) { - tickingblockentity.tick(); -+ // Paper start - execute chunk tasks during tick -+ if ((this.tileTickPosition & 7) == 0) { -+ MinecraftServer.getServer().executeMidTickTasks(); -+ } -+ // Paper end - execute chunk tasks during tick - } - } - this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 -@@ -929,6 +934,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public void guardEntityTick(Consumer tickConsumer, T entity) { - try { - tickConsumer.accept(entity); -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick - } catch (Throwable throwable) { - if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes diff --git a/patches/unapplied/server/1021-Optimise-random-block-ticking.patch b/patches/unapplied/server/1021-Optimise-random-block-ticking.patch deleted file mode 100644 index 36af1b75bf..0000000000 --- a/patches/unapplied/server/1021-Optimise-random-block-ticking.patch +++ /dev/null @@ -1,456 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 20 Jun 2021 16:19:26 -0700 -Subject: [PATCH] Optimise random block ticking - -Massive performance improvement for random block ticking. -The performance increase comes from the fact that the vast -majority of attempted block ticks (~95% in my testing) fail -because the randomly selected block is not tickable. - -Now only tickable blocks are targeted, however this means that -the maximum number of block ticks occurs per chunk. However, -not all chunks are going to be targeted. The percent chance -of a chunk being targeted is based on how many tickable blocks -are in the chunk. -This means that while block ticks are spread out less, the -total number of blocks ticked per world tick remains the same. -Therefore, the chance of a random tickable block being ticked -remains the same. - -diff --git a/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7d93652c1abbb6aee6eb7c26cf35d4d032ef7b69 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java -@@ -0,0 +1,65 @@ -+package io.papermc.paper.util.math; -+ -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.levelgen.LegacyRandomSource; -+import net.minecraft.world.level.levelgen.PositionalRandomFactory; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public final class ThreadUnsafeRandom extends LegacyRandomSource { -+ -+ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them. -+ private static final long multiplier = 0x5DEECE66DL; -+ private static final long addend = 0xBL; -+ private static final long mask = (1L << 48) - 1; -+ -+ private static long initialScramble(long seed) { -+ return (seed ^ multiplier) & mask; -+ } -+ -+ private long seed; -+ -+ public ThreadUnsafeRandom(long seed) { -+ super(seed); -+ } -+ -+ @Override -+ public RandomSource fork() { -+ return new ThreadUnsafeRandom(this.nextLong()); -+ } -+ -+ @Override -+ public PositionalRandomFactory forkPositional() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setSeed(long seed) { -+ // note: called by Random constructor -+ this.seed = initialScramble(seed); -+ } -+ -+ @Override -+ public int next(int bits) { -+ // avoid the expensive CAS logic used by superclass -+ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits)); -+ } -+ -+ // Taken from -+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ -+ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c -+ // Original license is public domain -+ public static int fastRandomBounded(final long randomInteger, final long limit) { -+ // randomInteger must be [0, pow(2, 32)) -+ // limit must be [0, pow(2, 32)) -+ return (int)((randomInteger * limit) >>> 32); -+ } -+ -+ @Override -+ public int nextInt(int bound) { -+ // yes this breaks random's spec -+ // however there's nothing that uses this class that relies on it -+ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 3a49f8933bc6ca1862994b7e0b3006f5236dd94a..bdad61438e7fd89f5f0cac6632dd395fcf024361 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -861,6 +861,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - entityplayer.stopSleepInBed(false, false); - }); - } -+ // Paper start - optimise random block ticking -+ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); -+ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); -+ // Paper end - - public void tickChunk(LevelChunk chunk, int randomTickSpeed) { - ChunkPos chunkcoordintpair = chunk.getPos(); -@@ -870,8 +874,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - ProfilerFiller gameprofilerfiller = this.getProfiler(); - - gameprofilerfiller.push("thunder"); -+ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change -+ - 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)); -+ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper - - if (this.isRainingAt(blockposition)) { - DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); -@@ -903,7 +909,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - 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 start -+ this.getRandomBlockPosition(j, 0, k, 15, blockposition); -+ this.tickPrecipitation(blockposition, chunk); -+ // Paper end - } - } - } // Paper - Option to disable ice and snow -@@ -911,36 +920,37 @@ public class ServerLevel extends Level implements WorldGenLevel { - gameprofilerfiller.popPush("tickBlocks"); - timings.chunkTicksBlocks.startTiming(); // Paper - if (randomTickSpeed > 0) { -- LevelChunkSection[] achunksection = chunk.getSections(); -- -- for (int i1 = 0; i1 < achunksection.length; ++i1) { -- LevelChunkSection chunksection = achunksection[i1]; -- -- if (chunksection.isRandomlyTicking()) { -- int j1 = chunk.getSectionYFromSectionIndex(i1); -- int k1 = SectionPos.sectionToBlockCoord(j1); -- -- for (int l1 = 0; l1 < randomTickSpeed; ++l1) { -- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15); -- -- gameprofilerfiller.push("randomTick"); -- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k); -- -- if (iblockdata.isRandomlyTicking()) { -- iblockdata.randomTick(this, blockposition1, this.random); -- } -+ // Paper start - optimize random block ticking -+ LevelChunkSection[] sections = chunk.getSections(); -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); -+ for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { -+ LevelChunkSection section = sections[sectionIndex]; -+ if (section == null || section.tickingList.size() == 0) continue; -+ -+ int yPos = (sectionIndex + minSection) << 4; -+ for (int a = 0; a < randomTickSpeed; ++a) { -+ int tickingBlocks = section.tickingList.size(); -+ int index = this.randomTickRandom.nextInt(16 * 16 * 16); -+ if (index >= tickingBlocks) { -+ continue; -+ } - -- FluidState fluid = iblockdata.getFluidState(); -+ long raw = section.tickingList.getRaw(index); -+ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); -+ int randomX = location & 15; -+ int randomY = ((location >>> (4 + 4)) & 255) | yPos; -+ int randomZ = (location >>> 4) & 15; - -- if (fluid.isRandomlyTicking()) { -- fluid.randomTick(this, blockposition1, this.random); -- } -+ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); -+ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); - -- gameprofilerfiller.pop(); -- } -+ iblockdata.randomTick(this, blockposition2, this.randomTickRandom); - } -+ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). -+ // TODO CHECK ON UPDATE (ping the Canadian) - } - } -+ // Paper end - optimise random block ticking - - timings.chunkTicksBlocks.stopTiming(); // Paper - gameprofilerfiller.pop(); -@@ -948,17 +958,25 @@ public class ServerLevel extends Level implements WorldGenLevel { - - @VisibleForTesting - public void tickPrecipitation(BlockPos pos) { -- BlockPos blockposition1 = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos); -- BlockPos blockposition2 = blockposition1.below(); -+ // Paper start - optimise chunk ticking -+ tickPrecipitation(pos.mutable(), this.getChunkAt(pos)); -+ } -+ public void tickPrecipitation(BlockPos.MutableBlockPos blockposition1, final LevelChunk chunk) { -+ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition1.getX() & 15, blockposition1.getZ() & 15) + 1; -+ int downY = normalY - 1; -+ blockposition1.setY(normalY); -+ // Paper end - optimise chunk ticking - Biome biomebase = (Biome) this.getBiome(blockposition1).value(); - -- if (biomebase.shouldFreeze(this, blockposition2)) { -- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition2, Blocks.ICE.defaultBlockState(), null); // CraftBukkit -+ blockposition1.setY(downY); -+ if (biomebase.shouldFreeze(this, blockposition1)) { -+ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit - } - - if (this.isRaining()) { - int i = this.getGameRules().getInt(GameRules.RULE_SNOW_ACCUMULATION_HEIGHT); - -+ blockposition1.setY(normalY); // Paper - optimise chunk ticking - if (i > 0 && biomebase.shouldSnow(this, blockposition1)) { - BlockState iblockdata = this.getBlockState(blockposition1); - -@@ -976,12 +994,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - } - -- Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition2); -+ blockposition1.setY(downY); // Paper - optimise chunk ticking -+ Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition1); // Paper - optimise chunk ticking - - if (biomebase_precipitation != Biome.Precipitation.NONE) { -- BlockState iblockdata2 = this.getBlockState(blockposition2); -+ BlockState iblockdata2 = this.getBlockState(blockposition1); // Paper - optimise chunk ticking - -- iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition2, biomebase_precipitation); -+ iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition1, biomebase_precipitation); // Paper - optimise chunk ticking - } - } - -diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java -index 68648c5a5e3ff079f832092af0f2f801c42d1ede..8bafd5fd7499ba4a04bf706cfd1e156073716e21 100644 ---- a/src/main/java/net/minecraft/util/BitStorage.java -+++ b/src/main/java/net/minecraft/util/BitStorage.java -@@ -20,4 +20,15 @@ public interface BitStorage { - void unpack(int[] out); - - BitStorage copy(); -+ -+ // Paper start -+ void forEach(DataBitConsumer consumer); -+ -+ @FunctionalInterface -+ interface DataBitConsumer { -+ -+ void accept(int location, int data); -+ -+ } -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java -index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8d7d763bf51cac556057645e6169c9447993189b 100644 ---- a/src/main/java/net/minecraft/util/SimpleBitStorage.java -+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java -@@ -315,6 +315,28 @@ public class SimpleBitStorage implements BitStorage { - return this.bits; - } - -+ // Paper start -+ @Override -+ public final void forEach(DataBitConsumer consumer) { -+ int i = 0; -+ long[] along = this.data; -+ int j = along.length; -+ -+ for (int k = 0; k < j; ++k) { -+ long l = along[k]; -+ -+ for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { -+ consumer.accept(i, (int) (l & this.mask)); -+ l >>= this.bits; -+ ++i; -+ if (i >= this.size) { -+ return; -+ } -+ } -+ } -+ } -+ // Paper end -+ - @Override - public void getAll(IntConsumer action) { - int i = 0; -diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java -index 50040c497a819cd1229042ab3cb057d34a32cacc..01f5b946fabbe34f31110e75973dab9f39897346 100644 ---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java -+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java -@@ -46,6 +46,15 @@ public class ZeroBitStorage implements BitStorage { - return 0; - } - -+ // Paper start -+ @Override -+ public void forEach(DataBitConsumer consumer) { -+ for(int i = 0; i < this.size; ++i) { -+ consumer.accept(i, 0); -+ } -+ } -+ // Paper end -+ - @Override - public void getAll(IntConsumer action) { - for (int i = 0; i < this.size; i++) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -index ffffa53fcaa6ec8681b283fa20bb5a3320ad9c11..30b87b5cb18c25cdd04eab64cfbe5acd6c1b6d84 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -@@ -88,7 +88,7 @@ public class Turtle extends Animal { - } - - public void setHomePos(BlockPos pos) { -- this.entityData.set(Turtle.HOME_POS, pos); -+ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... - } - - public BlockPos getHomePos() { -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index dab55ab08665f2b5ae0c899a4ab07c18460552ae..4001bec504b8fcda1eec03f49abd4e4d82cf6336 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1387,10 +1387,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public abstract RecipeManager getRecipeManager(); - - public BlockPos getBlockRandomPos(int x, int y, int z, int l) { -+ // Paper start - allow use of mutable pos -+ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); -+ this.getRandomBlockPosition(x, y, z, l, ret); -+ return ret.immutable(); -+ } -+ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { -+ // Paper end - this.randValue = this.randValue * 3 + 1013904223; - int i1 = this.randValue >> 2; - -- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); -+ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call -+ return out; // Paper - } - - public boolean noSave() { -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 f2e11bff414b521295bde721e01ae2166a6a3fd6..8cd6c1d838e0332125fde3fc36475034aa4effa0 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -@@ -25,6 +25,7 @@ public class LevelChunkSection { - public final PalettedContainer states; - // CraftBukkit start - read/write - private PalettedContainer> biomes; -+ public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper - - public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { - // CraftBukkit end -@@ -77,6 +78,9 @@ public class LevelChunkSection { - --this.nonEmptyBlockCount; - if (iblockdata1.isRandomlyTicking()) { - --this.tickingBlockCount; -+ // Paper start -+ this.tickingList.remove(x, y, z); -+ // Paper end - } - } - -@@ -88,6 +92,9 @@ public class LevelChunkSection { - ++this.nonEmptyBlockCount; - if (state.isRandomlyTicking()) { - ++this.tickingBlockCount; -+ // Paper start -+ this.tickingList.add(x, y, z, state); -+ // Paper end - } - } - -@@ -115,40 +122,34 @@ public class LevelChunkSection { - } - - public void recalcBlockCounts() { -- class a implements PalettedContainer.CountConsumer { -- -- public int nonEmptyBlockCount; -- public int tickingBlockCount; -- public int tickingFluidCount; -- -- a(final LevelChunkSection chunksection) {} -- -- public void accept(BlockState iblockdata, int i) { -+ // Paper start - unfuck this -+ this.tickingList.clear(); -+ this.nonEmptyBlockCount = 0; -+ this.tickingBlockCount = 0; -+ this.tickingFluidCount = 0; -+ // Don't run this on clearly empty sections -+ if (this.maybeHas((BlockState state) -> !state.isAir() || !state.getFluidState().isEmpty())) { -+ this.states.forEachLocation((BlockState iblockdata, int i) -> { - FluidState fluid = iblockdata.getFluidState(); - - if (!iblockdata.isAir()) { -- this.nonEmptyBlockCount += i; -+ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); - if (iblockdata.isRandomlyTicking()) { -- this.tickingBlockCount += i; -+ this.tickingBlockCount = (short)(this.tickingBlockCount + 1); -+ this.tickingList.add(i, iblockdata); - } - } - - if (!fluid.isEmpty()) { -- this.nonEmptyBlockCount += i; -+ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); - if (fluid.isRandomlyTicking()) { -- this.tickingFluidCount += i; -+ this.tickingFluidCount = (short) (this.tickingFluidCount + 1); - } - } - -- } -+ }); - } -- -- a a0 = new a(this); -- -- this.states.count(a0); -- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; -- this.tickingBlockCount = (short) a0.tickingBlockCount; -- this.tickingFluidCount = (short) a0.tickingFluidCount; -+ // Paper end - unfuck this - } - - public PalettedContainer getStates() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index 926c81a25180d508d662eb3fa35f771636164694..81368bf186365878db2e1ed305bb7bf36c26f61f 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -381,6 +381,14 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - } - } - -+ // Paper start -+ public void forEachLocation(PalettedContainer.CountConsumer consumer) { -+ this.data.storage.forEach((int location, int data) -> { -+ consumer.accept(this.data.palette.valueFor(data), location); -+ }); -+ } -+ // Paper end -+ - @FunctionalInterface - public interface CountConsumer { - void accept(T object, int count); diff --git a/patches/unapplied/server/1026-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/unapplied/server/1026-Optimise-collision-checking-in-player-move-packet-ha.patch deleted file mode 100644 index 7faeede880..0000000000 --- a/patches/unapplied/server/1026-Optimise-collision-checking-in-player-move-packet-ha.patch +++ /dev/null @@ -1,170 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 2 Jul 2020 12:02:43 -0700 -Subject: [PATCH] Optimise collision checking in player move packet handling - -Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision - -CHECK ME - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 34d4243fb5198e36ea3291ffe8ed2480bc960cdb..56d32a919cff2a9d4a5775665ed4d1d14b9c708b 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -572,7 +572,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - return; - } - -- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); -+ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet - - d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above -@@ -588,6 +588,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); -+ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - double d11 = d7; - - d6 = d3 - entity.getX(); -@@ -601,15 +602,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - boolean flag2 = false; - - if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot -- flag2 = true; -+ flag2 = true; // Paper - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); - } - - entity.absMoveTo(d3, d4, d5, f, f1); - this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); - -- if (flag && (flag2 || !flag3)) { -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = flag2; // violating this is always a fail -+ if (!teleportBack) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ AABB newBox = entity.getBoundingBox(); -+ if (didCollide || !oldBox.equals(newBox)) { -+ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ if (teleportBack) { // Paper end - optimise out extra getCubes - entity.absMoveTo(d0, d1, d2, f, f1); - this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit - this.send(new ClientboundMoveVehiclePacket(entity)); -@@ -688,7 +697,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - private boolean noBlocksAround(Entity entity) { -- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); -+ // Paper start - stop using streams, this is already a known fixed problem in Entity#move -+ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); -+ int minX = Mth.floor(box.minX); -+ int minY = Mth.floor(box.minY); -+ int minZ = Mth.floor(box.minZ); -+ int maxX = Mth.floor(box.maxX); -+ int maxY = Mth.floor(box.maxY); -+ int maxZ = Mth.floor(box.maxZ); -+ -+ Level world = entity.level(); -+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); -+ -+ for (int y = minY; y <= maxY; ++y) { -+ for (int z = minZ; z <= maxZ; ++z) { -+ for (int x = minX; x <= maxX; ++x) { -+ pos.set(x, y, z); -+ BlockState type = world.getBlockStateIfLoaded(pos); -+ if (type != null && !type.isAir()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return true; -+ // Paper end - stop using streams, this is already a known fixed problem in Entity#move - } - - @Override -@@ -1283,7 +1317,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - if (this.awaitingPositionFromClient != null) { -- if (this.tickCount - this.awaitingTeleportTime > 20) { -+ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT - this.awaitingTeleportTime = this.tickCount; - this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); - } -@@ -1392,7 +1426,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -- AABB axisalignedbb = this.player.getBoundingBox(); -+ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB - - d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above -@@ -1434,6 +1468,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); - this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move -+ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - // Paper start - prevent position desync - if (this.awaitingPositionFromClient != null) { - return; // ... thanks Mojang for letting move calls teleport across dimensions. -@@ -1464,7 +1499,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - // Paper start - Add fail move event -- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; -+ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang -+ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { -+ AABB newBox = this.player.getBoundingBox(); -+ if (didCollide || !axisalignedbb.equals(newBox)) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ // Paper end - optimise out extra getCubes - if (teleportBack) { - io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, - toX, toY, toZ, toYaw, toPitch, false); -@@ -1570,6 +1615,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -+ // Paper start - optimise out extra getCubes -+ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { -+ final List collisionsBB = new java.util.ArrayList<>(); -+ final List collisionsVoxel = new java.util.ArrayList<>(); -+ io.papermc.paper.util.CollisionUtil.getCollisions( -+ world, entity, newBox, collisionsVoxel, collisionsBB, -+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null, null -+ ); -+ -+ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { -+ final AABB box = collisionsBB.get(i); -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { -+ return true; -+ } -+ } -+ -+ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { -+ final VoxelShape voxel = collisionsVoxel.get(i); -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ // Paper end - optimise out extra getCubes - private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { - AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); - Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/unapplied/server/1027-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/unapplied/server/1027-Optimise-collision-checking-in-player-move-packet-ha.patch new file mode 100644 index 0000000000..1e3006e3e6 --- /dev/null +++ b/patches/unapplied/server/1027-Optimise-collision-checking-in-player-move-packet-ha.patch @@ -0,0 +1,170 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 2 Jul 2020 12:02:43 -0700 +Subject: [PATCH] Optimise collision checking in player move packet handling + +Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision + +CHECK ME + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 7796e191747be545e744564a2b0b65790f69114d..2b94b2dc3f29e7005b1a30c83f4a68e1263780b4 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -572,7 +572,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + return; + } + +- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); ++ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet + + d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above +@@ -588,6 +588,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); ++ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + double d11 = d7; + + d6 = d3 - entity.getX(); +@@ -601,15 +602,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + boolean flag2 = false; + + if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot +- flag2 = true; ++ flag2 = true; // Paper - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); + } + + entity.absMoveTo(d3, d4, d5, f, f1); + this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit +- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); + +- if (flag && (flag2 || !flag3)) { ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = flag2; // violating this is always a fail ++ if (!teleportBack) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ AABB newBox = entity.getBoundingBox(); ++ if (didCollide || !oldBox.equals(newBox)) { ++ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ if (teleportBack) { // Paper end - optimise out extra getCubes + entity.absMoveTo(d0, d1, d2, f, f1); + this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + this.send(new ClientboundMoveVehiclePacket(entity)); +@@ -691,7 +700,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + private boolean noBlocksAround(Entity entity) { +- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); ++ // Paper start - stop using streams, this is already a known fixed problem in Entity#move ++ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); ++ int minX = Mth.floor(box.minX); ++ int minY = Mth.floor(box.minY); ++ int minZ = Mth.floor(box.minZ); ++ int maxX = Mth.floor(box.maxX); ++ int maxY = Mth.floor(box.maxY); ++ int maxZ = Mth.floor(box.maxZ); ++ ++ Level world = entity.level(); ++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ ++ for (int y = minY; y <= maxY; ++y) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ for (int x = minX; x <= maxX; ++x) { ++ pos.set(x, y, z); ++ BlockState type = world.getBlockStateIfLoaded(pos); ++ if (type != null && !type.isAir()) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ // Paper end - stop using streams, this is already a known fixed problem in Entity#move + } + + @Override +@@ -1387,7 +1421,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + +- AABB axisalignedbb = this.player.getBoundingBox(); ++ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB + + d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above +@@ -1429,6 +1463,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); + this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move ++ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + // Paper start - prevent position desync + if (this.awaitingPositionFromClient != null) { + return; // ... thanks Mojang for letting move calls teleport across dimensions. +@@ -1459,7 +1494,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + // Paper start - Add fail move event +- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; ++ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang ++ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { ++ AABB newBox = this.player.getBoundingBox(); ++ if (didCollide || !axisalignedbb.equals(newBox)) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ // Paper end - optimise out extra getCubes + if (teleportBack) { + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, + toX, toY, toZ, toYaw, toPitch, false); +@@ -1570,7 +1615,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + private boolean updateAwaitingTeleport() { + if (this.awaitingPositionFromClient != null) { +- if (this.tickCount - this.awaitingTeleportTime > 20) { ++ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT + this.awaitingTeleportTime = this.tickCount; + this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); + } +@@ -1583,6 +1628,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + ++ // Paper start - optimise out extra getCubes ++ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { ++ final List collisionsBB = new java.util.ArrayList<>(); ++ final List collisionsVoxel = new java.util.ArrayList<>(); ++ io.papermc.paper.util.CollisionUtil.getCollisions( ++ world, entity, newBox, collisionsVoxel, collisionsBB, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); ++ ++ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { ++ final AABB box = collisionsBB.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { ++ return true; ++ } ++ } ++ ++ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { ++ final VoxelShape voxel = collisionsVoxel.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Paper end - optimise out extra getCubes + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { + AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); + Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/unapplied/server/1029-optimize-dirt-and-snow-spreading.patch b/patches/unapplied/server/1029-optimize-dirt-and-snow-spreading.patch deleted file mode 100644 index 49de7fcab9..0000000000 --- a/patches/unapplied/server/1029-optimize-dirt-and-snow-spreading.patch +++ /dev/null @@ -1,78 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: lukas81298 -Date: Fri, 22 Jan 2021 21:50:18 +0100 -Subject: [PATCH] optimize dirt and snow spreading - - -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 5a39e8d359dc13383711e49ffb2d1294dad26192..b7165ec19bef1a07f6618fc0429d86cda1b08da4 100644 ---- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -@@ -18,8 +18,13 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - } - - private static boolean canBeGrass(BlockState state, LevelReader world, BlockPos pos) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ return canBeGrass(world.getChunk(pos), state, world, pos); -+ } -+ private static boolean canBeGrass(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { -+ // Paper end - Perf: optimize dirt and snow spreading - BlockPos blockposition1 = pos.above(); -- BlockState iblockdata1 = world.getBlockState(blockposition1); -+ BlockState iblockdata1 = chunk.getBlockState(blockposition1); // Paper - Perf: optimize dirt and snow spreading - - if (iblockdata1.is(Blocks.SNOW) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 1) { - return true; -@@ -36,15 +41,27 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - protected abstract MapCodec codec(); - - private static boolean canPropagate(BlockState state, LevelReader world, BlockPos pos) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ return canPropagate(world.getChunk(pos), state, world, pos); -+ } -+ -+ private static boolean canPropagate(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { -+ // Paper end - Perf: optimize dirt and snow spreading - BlockPos blockposition1 = pos.above(); - -- return SpreadingSnowyDirtBlock.canBeGrass(state, world, pos) && !world.getFluidState(blockposition1).is(FluidTags.WATER); -+ return SpreadingSnowyDirtBlock.canBeGrass(chunk, state, world, pos) && !chunk.getFluidState(blockposition1).is(FluidTags.WATER); // Paper - Perf: optimize dirt and snow spreading - } - - @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)) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); -+ if (cachedBlockChunk == null) { // Is this needed? -+ return; -+ } -+ if (!SpreadingSnowyDirtBlock.canBeGrass(cachedBlockChunk, state, world, pos)) { -+ // Paper end - Perf: optimize dirt and snow spreading - // CraftBukkit start - if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { - return; -@@ -57,9 +74,19 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - - for (int i = 0; i < 4; ++i) { - BlockPos blockposition1 = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1); -- -- if (world.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(iblockdata1, world, blockposition1)) { -- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, world.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit -+ // Paper start - Perf: optimize dirt and snow spreading -+ if (pos.getX() == blockposition1.getX() && pos.getY() == blockposition1.getY() && pos.getZ() == blockposition1.getZ()) { -+ continue; -+ } -+ net.minecraft.world.level.chunk.ChunkAccess access; -+ if (cachedBlockChunk.locX == blockposition1.getX() >> 4 && cachedBlockChunk.locZ == blockposition1.getZ() >> 4) { -+ access = cachedBlockChunk; -+ } else { -+ access = world.getChunkAt(blockposition1); -+ } -+ if (access.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(access, iblockdata1, world, blockposition1)) { -+ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, access.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit -+ // Paper end - Perf: optimize dirt and snow spreading - } - } - } diff --git a/patches/unapplied/server/1033-Actually-optimise-explosions.patch b/patches/unapplied/server/1033-Actually-optimise-explosions.patch deleted file mode 100644 index fa8536ea3c..0000000000 --- a/patches/unapplied/server/1033-Actually-optimise-explosions.patch +++ /dev/null @@ -1,521 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Tue, 12 Sep 2023 06:50:16 -0700 -Subject: [PATCH] Actually optimise explosions - -The vast majority of blocks an explosion of power ~4 tries -to destroy are duplicates. The core of the block destroying -part of this patch is to cache the block state, resistance, and -whether it should explode - as those will not change. - -The other part of this patch is to optimise the visibility -percentage calculation. The new visibility calculation takes -advantage of the block caching already done by the explosion logic. -It continues to update the cache as the visibility calculation -uses many rays which can overlap significantly. - -Effectively, the patch uses a lot of caching to eliminate -redundant operations. - -Performance benchmarking explosions is challenging, as it varies -depending on the power, the number of nearby entities, and the -nearby terrain. This means that no benchmark can cover all the cases. -I decided to test a giant block of TNT, as that's where the optimisations -would be needed the most. - -I tested using a 50x10x50 block of TNT above ground -and determined the following: - -Vanilla time per explosion: 2.27ms -Lithium time per explosion: 1.07ms -This patch time per explosion: 0.45ms - -The results indicate that this logic is 5 times faster than Vanilla -and 2.3 times faster than Lithium. - -diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java -index ef40b996864c81d7e8fbb0727ea0a96f866c725f..638447e8cb74b26f4f8e3862c2fb4a3048979ebc 100644 ---- a/src/main/java/net/minecraft/world/level/Explosion.java -+++ b/src/main/java/net/minecraft/world/level/Explosion.java -@@ -112,6 +112,271 @@ public class Explosion { - this.yield = this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F; // CraftBukkit - } - -+ // Paper start - optimise collisions -+ private static final double[] CACHED_RAYS; -+ static { -+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); -+ -+ for (int x = 0; x <= 15; ++x) { -+ for (int y = 0; y <= 15; ++y) { -+ for (int z = 0; z <= 15; ++z) { -+ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { -+ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); -+ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); -+ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); -+ -+ double mag = Math.sqrt( -+ xDir * xDir + yDir * yDir + zDir * zDir -+ ); -+ -+ rayCoords.add((xDir / mag) * (double)0.3F); -+ rayCoords.add((yDir / mag) * (double)0.3F); -+ rayCoords.add((zDir / mag) * (double)0.3F); -+ } -+ } -+ } -+ } -+ -+ CACHED_RAYS = rayCoords.toDoubleArray(); -+ } -+ -+ private static final int CHUNK_CACHE_SHIFT = 2; -+ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; -+ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; -+ -+ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; -+ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; -+ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; -+ -+ // resistance = (res + 0.3F) * 0.3F; -+ // so for resistance = 0, we need res = -0.3F -+ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); -+ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; -+ -+ public static final class ExplosionBlockCache { -+ -+ public final long key; -+ public final BlockPos immutablePos; -+ public final BlockState blockState; -+ public final FluidState fluidState; -+ public final float resistance; -+ public final boolean outOfWorld; -+ public Boolean shouldExplode; // null -> not called yet -+ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; -+ -+ public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, -+ boolean outOfWorld) { -+ this.key = key; -+ this.immutablePos = immutablePos; -+ this.blockState = blockState; -+ this.fluidState = fluidState; -+ this.resistance = resistance; -+ this.outOfWorld = outOfWorld; -+ } -+ } -+ -+ private long[] chunkPosCache = null; -+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; -+ -+ private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, -+ final long key, final boolean calculateResistance) { -+ ExplosionBlockCache ret = this.blockCache.get(key); -+ if (ret != null) { -+ return ret; -+ } -+ -+ BlockPos pos = new BlockPos(x, y, z); -+ -+ if (!this.level.isInWorldBounds(pos)) { -+ ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); -+ } else { -+ net.minecraft.world.level.chunk.LevelChunk chunk; -+ long chunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); -+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); -+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { -+ chunk = this.chunkCache[chunkCacheKey]; -+ } else { -+ this.chunkPosCache[chunkCacheKey] = chunkKey; -+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); -+ } -+ -+ BlockState blockState = chunk.getBlockStateFinal(x, y, z); -+ FluidState fluidState = blockState.getFluidState(); -+ -+ Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); -+ -+ ret = new ExplosionBlockCache( -+ key, pos, blockState, fluidState, -+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, -+ false -+ ); -+ } -+ -+ this.blockCache.put(key, ret); -+ -+ return ret; -+ } -+ -+ private boolean clipsAnything(final Vec3 from, final Vec3 to, -+ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context, -+ final ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos currPos) { -+ // assume that context.delegated = false -+ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); -+ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); -+ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); -+ -+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { -+ return false; -+ } -+ -+ final double toXAdj = to.x - adjX; -+ final double toYAdj = to.y - adjY; -+ final double toZAdj = to.z - adjZ; -+ final double fromXAdj = from.x + adjX; -+ final double fromYAdj = from.y + adjY; -+ final double fromZAdj = from.z + adjZ; -+ -+ int currX = Mth.floor(fromXAdj); -+ int currY = Mth.floor(fromYAdj); -+ int currZ = Mth.floor(fromZAdj); -+ -+ final double diffX = toXAdj - fromXAdj; -+ final double diffY = toYAdj - fromYAdj; -+ final double diffZ = toZAdj - fromZAdj; -+ -+ final double dxDouble = Math.signum(diffX); -+ final double dyDouble = Math.signum(diffY); -+ final double dzDouble = Math.signum(diffZ); -+ -+ final int dx = (int)dxDouble; -+ final int dy = (int)dyDouble; -+ final int dz = (int)dzDouble; -+ -+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; -+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; -+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; -+ -+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); -+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); -+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); -+ -+ for (;;) { -+ currPos.set(currX, currY, currZ); -+ -+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape -+ // ClipContext.Fluid.NONE -> ignore fluids -+ -+ // read block from cache -+ final long key = BlockPos.asLong(currX, currY, currZ); -+ -+ final int cacheKey = -+ (currX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ ExplosionBlockCache cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); -+ } -+ -+ final BlockState blockState = cachedBlock.blockState; -+ if (blockState != null && !blockState.emptyCollisionShape()) { -+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; -+ if (collision == null) { -+ collision = blockState.getConstantCollisionShape(); -+ if (collision == null) { -+ collision = blockState.getCollisionShape(this.level, currPos, context); -+ if (!context.isDelegated()) { -+ // if it was not delegated during this call, assume that for any future ones it will not be delegated -+ // again, and cache the result -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } else { -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } -+ -+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { -+ return true; -+ } -+ } -+ -+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { -+ return false; -+ } -+ -+ // inc the smallest normalized coordinate -+ -+ if (normalizedCurrX < normalizedCurrY) { -+ if (normalizedCurrX < normalizedCurrZ) { -+ currX += dx; -+ normalizedCurrX += normalizedDiffX; -+ } else { -+ // x < y && x >= z <--> z < y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } else if (normalizedCurrY < normalizedCurrZ) { -+ // y <= x && y < z -+ currY += dy; -+ normalizedCurrY += normalizedDiffY; -+ } else { -+ // y <= x && z <= y <--> z <= y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } -+ } -+ -+ private float getSeenFraction(final Vec3 source, final Entity target, -+ final ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos blockPos) { -+ final AABB boundingBox = target.getBoundingBox(); -+ final double diffX = boundingBox.maxX - boundingBox.minX; -+ final double diffY = boundingBox.maxY - boundingBox.minY; -+ final double diffZ = boundingBox.maxZ - boundingBox.minZ; -+ -+ final double incX = 1.0 / (diffX * 2.0 + 1.0); -+ final double incY = 1.0 / (diffY * 2.0 + 1.0); -+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); -+ -+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { -+ return 0.0f; -+ } -+ -+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; -+ final double offY = boundingBox.minY; -+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; -+ -+ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(target); -+ -+ int totalRays = 0; -+ int missedRays = 0; -+ -+ for (double dx = 0.0; dx <= 1.0; dx += incX) { -+ final double fromX = Math.fma(dx, diffX, offX); -+ for (double dy = 0.0; dy <= 1.0; dy += incY) { -+ final double fromY = Math.fma(dy, diffY, offY); -+ for (double dz = 0.0; dz <= 1.0; dz += incZ) { -+ ++totalRays; -+ -+ final Vec3 from = new Vec3( -+ fromX, -+ fromY, -+ Math.fma(dz, diffZ, offZ) -+ ); -+ -+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { -+ ++missedRays; -+ } -+ } -+ } -+ } -+ -+ return (float)missedRays / (float)totalRays; -+ } -+ // Paper end - optimise collisions -+ - private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { - return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); - } -@@ -172,40 +437,88 @@ public class Explosion { - int i; - int j; - -- for (int k = 0; k < 16; ++k) { -- for (i = 0; i < 16; ++i) { -- for (j = 0; j < 16; ++j) { -- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { -- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); -- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); -- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); -- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); -- -- d0 /= d3; -- d1 /= d3; -- d2 /= d3; -+ // Paper start - optimise explosions -+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); -+ -+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); -+ -+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ -+ final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; -+ // use initial cache value that is most likely to be used: the source position -+ final ExplosionBlockCache initialCache; -+ { -+ final int blockX = Mth.floor(this.x); -+ final int blockY = Mth.floor(this.y); -+ final int blockZ = Mth.floor(this.z); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of -+ // a 16x16x16 cube -+ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and -+ // calculations in one go -+ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most -+ // block retrievals are not unique -+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { -+ { -+ { -+ { -+ ExplosionBlockCache cachedBlock = initialCache; -+ -+ double d0 = CACHED_RAYS[ray]; -+ double d1 = CACHED_RAYS[ray + 1]; -+ double d2 = CACHED_RAYS[ray + 2]; -+ ray += 3; -+ // Paper end - optimise explosions - float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); - double d4 = this.x; - double d5 = this.y; - double d6 = this.z; - - for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { -- BlockPos blockposition = BlockPos.containing(d4, d5, d6); -- BlockState iblockdata = this.level.getBlockState(blockposition); -- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed -- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions -+ // Paper start - optimise explosions -+ final int blockX = Mth.floor(d4); -+ final int blockY = Mth.floor(d5); -+ final int blockZ = Mth.floor(d6); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ if (cachedBlock.key != key) { -+ final int cacheKey = -+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ } - -- if (!this.level.isInWorldBounds(blockposition)) { -+ if (cachedBlock.outOfWorld) { - break; - } - -- Optional optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); -+ BlockPos blockposition = cachedBlock.immutablePos; -+ BlockState iblockdata = cachedBlock.blockState; -+ // Paper end - optimise explosions - -- if (optional.isPresent()) { -- f -= ((Float) optional.get() + 0.3F) * 0.3F; -- } -+ if (!iblockdata.isDestroyable()) continue; // Paper -+ // Paper - optimise explosions - -- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { -+ f -= cachedBlock.resistance; // Paper - optimise explosions -+ -+ if (f > 0.0F && cachedBlock.shouldExplode == null) { // Paper - optimise explosions -+ // Paper start - optimise explosions -+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. -+ // basically, it is unused, which allows us to cache the result -+ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode(this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, f); -+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; -+ if (shouldExplode && (this.fire || !cachedBlock.blockState.isAir())) { -+ // Paper end - optimise explosions - set.add(blockposition); - // Paper start - prevent headless pistons from forming - if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { -@@ -216,11 +529,12 @@ public class Explosion { - } - } - // Paper end - prevent headless pistons from forming -+ } // Paper - optimise explosions - } - -- d4 += d0 * 0.30000001192092896D; -- d5 += d1 * 0.30000001192092896D; -- d6 += d2 * 0.30000001192092896D; -+ d4 += d0; // Paper - optimise explosions -+ d5 += d1; // Paper - optimise explosions -+ d6 += d2; // Paper - optimise explosions - } - } - } -@@ -240,6 +554,8 @@ public class Explosion { - Vec3 vec3d = new Vec3(this.x, this.y, this.z); - Iterator iterator = list.iterator(); - -+ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions -+ - while (iterator.hasNext()) { - Entity entity = (Entity) iterator.next(); - -@@ -275,11 +591,11 @@ public class Explosion { - for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { - // Calculate damage separately for each EntityComplexPart - if (list.contains(entityComplexPart)) { -- entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); -+ entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entityComplexPart, getSeenFraction(vec3d, entityComplexPart, blockCache, blockPos))); // Paper - actually optimise explosions and use the right entity to calculate the damage - } - } - } else { -- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); -+ entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, getSeenFraction(vec3d, entity, blockCache, blockPos))); // Paper - actually optimise explosions - } - - if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled -@@ -288,7 +604,7 @@ public class Explosion { - // CraftBukkit end - } - -- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions -+ double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity, blockCache, blockPos) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions - double d13; - - if (entity instanceof LivingEntity) { -@@ -327,6 +643,9 @@ public class Explosion { - } - } - -+ this.blockCache = null; // Paper - optimise explosions -+ this.chunkPosCache = null; // Paper - optimise explosions -+ this.chunkCache = null; // Paper - optimise explosions - } - - public void finalizeExplosion(boolean particles) { -@@ -542,14 +861,14 @@ public class Explosion { - private BlockInteraction() {} - } - // Paper start - Optimize explosions -- private float getBlockDensity(Vec3 vec3d, Entity entity) { -+ private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions - if (!this.level.paperConfig().environment.optimizeExplosions) { -- return getSeenPercent(vec3d, entity); -+ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions - } - CacheKey key = new CacheKey(this, entity.getBoundingBox()); - Float blockDensity = this.level.explosionDensityCache.get(key); - if (blockDensity == null) { -- blockDensity = getSeenPercent(vec3d, entity); -+ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions; - this.level.explosionDensityCache.put(key, blockDensity); - } - -diff --git a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -index 0ef9b402d129b072134688c06719a56328581158..1eb259b48bcab6172c15546744eea410c6a3e1fe 100644 ---- a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -+++ b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -@@ -26,11 +26,17 @@ public class ExplosionDamageCalculator { - return 1.0F; - } - -+ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - public float getEntityDamageAmount(Explosion explosion, Entity entity) { -+ // Paper start - actually optimise explosions -+ return this.getEntityDamageAmount(explosion, entity, Explosion.getSeenPercent(explosion.center(), entity)); -+ } -+ public float getEntityDamageAmount(Explosion explosion, Entity entity, double seenPercent) { -+ // Paper end - actually optimise explosions - float f = explosion.radius() * 2.0F; - Vec3 vec3 = explosion.center(); - double d = Math.sqrt(entity.distanceToSqr(vec3)) / (double)f; -- double e = (1.0 - d) * (double)Explosion.getSeenPercent(vec3, entity); -+ double e = (1.0 - d) * seenPercent; // Paper - actually optimise explosions - return (float)((e * e + e) / 2.0 * 7.0 * (double)f + 1.0); - } - } diff --git a/patches/unapplied/server/1034-Actually-optimise-explosions.patch b/patches/unapplied/server/1034-Actually-optimise-explosions.patch new file mode 100644 index 0000000000..065129443d --- /dev/null +++ b/patches/unapplied/server/1034-Actually-optimise-explosions.patch @@ -0,0 +1,521 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 12 Sep 2023 06:50:16 -0700 +Subject: [PATCH] Actually optimise explosions + +The vast majority of blocks an explosion of power ~4 tries +to destroy are duplicates. The core of the block destroying +part of this patch is to cache the block state, resistance, and +whether it should explode - as those will not change. + +The other part of this patch is to optimise the visibility +percentage calculation. The new visibility calculation takes +advantage of the block caching already done by the explosion logic. +It continues to update the cache as the visibility calculation +uses many rays which can overlap significantly. + +Effectively, the patch uses a lot of caching to eliminate +redundant operations. + +Performance benchmarking explosions is challenging, as it varies +depending on the power, the number of nearby entities, and the +nearby terrain. This means that no benchmark can cover all the cases. +I decided to test a giant block of TNT, as that's where the optimisations +would be needed the most. + +I tested using a 50x10x50 block of TNT above ground +and determined the following: + +Vanilla time per explosion: 2.27ms +Lithium time per explosion: 1.07ms +This patch time per explosion: 0.45ms + +The results indicate that this logic is 5 times faster than Vanilla +and 2.3 times faster than Lithium. + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index bff83fe413c7baef4ba56a3270ea4463a58c792f..7aa9ddb1d61ffb7da3f867e5a5bd04e3432b5621 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -113,6 +113,271 @@ public class Explosion { + this.yield = this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F; // CraftBukkit + } + ++ // Paper start - optimise collisions ++ private static final double[] CACHED_RAYS; ++ static { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); ++ ++ for (int x = 0; x <= 15; ++x) { ++ for (int y = 0; y <= 15; ++y) { ++ for (int z = 0; z <= 15; ++z) { ++ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { ++ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); ++ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); ++ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); ++ ++ double mag = Math.sqrt( ++ xDir * xDir + yDir * yDir + zDir * zDir ++ ); ++ ++ rayCoords.add((xDir / mag) * (double)0.3F); ++ rayCoords.add((yDir / mag) * (double)0.3F); ++ rayCoords.add((zDir / mag) * (double)0.3F); ++ } ++ } ++ } ++ } ++ ++ CACHED_RAYS = rayCoords.toDoubleArray(); ++ } ++ ++ private static final int CHUNK_CACHE_SHIFT = 2; ++ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; ++ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; ++ ++ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; ++ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; ++ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; ++ ++ // resistance = (res + 0.3F) * 0.3F; ++ // so for resistance = 0, we need res = -0.3F ++ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); ++ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; ++ ++ public static final class ExplosionBlockCache { ++ ++ public final long key; ++ public final BlockPos immutablePos; ++ public final BlockState blockState; ++ public final FluidState fluidState; ++ public final float resistance; ++ public final boolean outOfWorld; ++ public Boolean shouldExplode; // null -> not called yet ++ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; ++ ++ public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, ++ boolean outOfWorld) { ++ this.key = key; ++ this.immutablePos = immutablePos; ++ this.blockState = blockState; ++ this.fluidState = fluidState; ++ this.resistance = resistance; ++ this.outOfWorld = outOfWorld; ++ } ++ } ++ ++ private long[] chunkPosCache = null; ++ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; ++ ++ private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, ++ final long key, final boolean calculateResistance) { ++ ExplosionBlockCache ret = this.blockCache.get(key); ++ if (ret != null) { ++ return ret; ++ } ++ ++ BlockPos pos = new BlockPos(x, y, z); ++ ++ if (!this.level.isInWorldBounds(pos)) { ++ ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); ++ } else { ++ net.minecraft.world.level.chunk.LevelChunk chunk; ++ long chunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); ++ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); ++ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { ++ chunk = this.chunkCache[chunkCacheKey]; ++ } else { ++ this.chunkPosCache[chunkCacheKey] = chunkKey; ++ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); ++ } ++ ++ BlockState blockState = chunk.getBlockStateFinal(x, y, z); ++ FluidState fluidState = blockState.getFluidState(); ++ ++ Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); ++ ++ ret = new ExplosionBlockCache( ++ key, pos, blockState, fluidState, ++ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, ++ false ++ ); ++ } ++ ++ this.blockCache.put(key, ret); ++ ++ return ret; ++ } ++ ++ private boolean clipsAnything(final Vec3 from, final Vec3 to, ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos currPos) { ++ // assume that context.delegated = false ++ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); ++ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); ++ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); ++ ++ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { ++ return false; ++ } ++ ++ final double toXAdj = to.x - adjX; ++ final double toYAdj = to.y - adjY; ++ final double toZAdj = to.z - adjZ; ++ final double fromXAdj = from.x + adjX; ++ final double fromYAdj = from.y + adjY; ++ final double fromZAdj = from.z + adjZ; ++ ++ int currX = Mth.floor(fromXAdj); ++ int currY = Mth.floor(fromYAdj); ++ int currZ = Mth.floor(fromZAdj); ++ ++ final double diffX = toXAdj - fromXAdj; ++ final double diffY = toYAdj - fromYAdj; ++ final double diffZ = toZAdj - fromZAdj; ++ ++ final double dxDouble = Math.signum(diffX); ++ final double dyDouble = Math.signum(diffY); ++ final double dzDouble = Math.signum(diffZ); ++ ++ final int dx = (int)dxDouble; ++ final int dy = (int)dyDouble; ++ final int dz = (int)dzDouble; ++ ++ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; ++ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; ++ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; ++ ++ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); ++ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); ++ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape ++ // ClipContext.Fluid.NONE -> ignore fluids ++ ++ // read block from cache ++ final long key = BlockPos.asLong(currX, currY, currZ); ++ ++ final int cacheKey = ++ (currX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ ExplosionBlockCache cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); ++ } ++ ++ final BlockState blockState = cachedBlock.blockState; ++ if (blockState != null && !blockState.emptyCollisionShape()) { ++ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; ++ if (collision == null) { ++ collision = blockState.getConstantCollisionShape(); ++ if (collision == null) { ++ collision = blockState.getCollisionShape(this.level, currPos, context); ++ if (!context.isDelegated()) { ++ // if it was not delegated during this call, assume that for any future ones it will not be delegated ++ // again, and cache the result ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } else { ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } ++ ++ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { ++ return true; ++ } ++ } ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return false; ++ } ++ ++ // inc the smallest normalized coordinate ++ ++ if (normalizedCurrX < normalizedCurrY) { ++ if (normalizedCurrX < normalizedCurrZ) { ++ currX += dx; ++ normalizedCurrX += normalizedDiffX; ++ } else { ++ // x < y && x >= z <--> z < y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } else if (normalizedCurrY < normalizedCurrZ) { ++ // y <= x && y < z ++ currY += dy; ++ normalizedCurrY += normalizedDiffY; ++ } else { ++ // y <= x && z <= y <--> z <= y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } ++ } ++ ++ private float getSeenFraction(final Vec3 source, final Entity target, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos blockPos) { ++ final AABB boundingBox = target.getBoundingBox(); ++ final double diffX = boundingBox.maxX - boundingBox.minX; ++ final double diffY = boundingBox.maxY - boundingBox.minY; ++ final double diffZ = boundingBox.maxZ - boundingBox.minZ; ++ ++ final double incX = 1.0 / (diffX * 2.0 + 1.0); ++ final double incY = 1.0 / (diffY * 2.0 + 1.0); ++ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); ++ ++ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { ++ return 0.0f; ++ } ++ ++ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; ++ final double offY = boundingBox.minY; ++ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; ++ ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(target); ++ ++ int totalRays = 0; ++ int missedRays = 0; ++ ++ for (double dx = 0.0; dx <= 1.0; dx += incX) { ++ final double fromX = Math.fma(dx, diffX, offX); ++ for (double dy = 0.0; dy <= 1.0; dy += incY) { ++ final double fromY = Math.fma(dy, diffY, offY); ++ for (double dz = 0.0; dz <= 1.0; dz += incZ) { ++ ++totalRays; ++ ++ final Vec3 from = new Vec3( ++ fromX, ++ fromY, ++ Math.fma(dz, diffZ, offZ) ++ ); ++ ++ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { ++ ++missedRays; ++ } ++ } ++ } ++ } ++ ++ return (float)missedRays / (float)totalRays; ++ } ++ // Paper end - optimise collisions ++ + private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { + return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); + } +@@ -173,40 +438,88 @@ public class Explosion { + int i; + int j; + +- for (int k = 0; k < 16; ++k) { +- for (i = 0; i < 16; ++i) { +- for (j = 0; j < 16; ++j) { +- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { +- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); +- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); +- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); +- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); +- +- d0 /= d3; +- d1 /= d3; +- d2 /= d3; ++ // Paper start - optimise explosions ++ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); ++ ++ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); ++ ++ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ ++ final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; ++ // use initial cache value that is most likely to be used: the source position ++ final ExplosionBlockCache initialCache; ++ { ++ final int blockX = Mth.floor(this.x); ++ final int blockY = Mth.floor(this.y); ++ final int blockZ = Mth.floor(this.z); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of ++ // a 16x16x16 cube ++ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and ++ // calculations in one go ++ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most ++ // block retrievals are not unique ++ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { ++ { ++ { ++ { ++ ExplosionBlockCache cachedBlock = initialCache; ++ ++ double d0 = CACHED_RAYS[ray]; ++ double d1 = CACHED_RAYS[ray + 1]; ++ double d2 = CACHED_RAYS[ray + 2]; ++ ray += 3; ++ // Paper end - optimise explosions + float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); + double d4 = this.x; + double d5 = this.y; + double d6 = this.z; + + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { +- BlockPos blockposition = BlockPos.containing(d4, d5, d6); +- BlockState iblockdata = this.level.getBlockState(blockposition); +- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed +- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions ++ // Paper start - optimise explosions ++ final int blockX = Mth.floor(d4); ++ final int blockY = Mth.floor(d5); ++ final int blockZ = Mth.floor(d6); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ if (cachedBlock.key != key) { ++ final int cacheKey = ++ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ } + +- if (!this.level.isInWorldBounds(blockposition)) { ++ if (cachedBlock.outOfWorld) { + break; + } + +- Optional optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); ++ BlockPos blockposition = cachedBlock.immutablePos; ++ BlockState iblockdata = cachedBlock.blockState; ++ // Paper end - optimise explosions + +- if (optional.isPresent()) { +- f -= ((Float) optional.get() + 0.3F) * 0.3F; +- } ++ if (!iblockdata.isDestroyable()) continue; // Paper ++ // Paper - optimise explosions + +- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { ++ f -= cachedBlock.resistance; // Paper - optimise explosions ++ ++ if (f > 0.0F && cachedBlock.shouldExplode == null) { // Paper - optimise explosions ++ // Paper start - optimise explosions ++ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. ++ // basically, it is unused, which allows us to cache the result ++ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode(this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, f); ++ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; ++ if (shouldExplode && (this.fire || !cachedBlock.blockState.isAir())) { ++ // Paper end - optimise explosions + set.add(blockposition); + // Paper start - prevent headless pistons from forming + if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { +@@ -217,11 +530,12 @@ public class Explosion { + } + } + // Paper end - prevent headless pistons from forming ++ } // Paper - optimise explosions + } + +- d4 += d0 * 0.30000001192092896D; +- d5 += d1 * 0.30000001192092896D; +- d6 += d2 * 0.30000001192092896D; ++ d4 += d0; // Paper - optimise explosions ++ d5 += d1; // Paper - optimise explosions ++ d6 += d2; // Paper - optimise explosions + } + } + } +@@ -241,6 +555,8 @@ public class Explosion { + Vec3 vec3d = new Vec3(this.x, this.y, this.z); + Iterator iterator = list.iterator(); + ++ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions ++ + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +@@ -276,11 +592,11 @@ public class Explosion { + for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { + // Calculate damage separately for each EntityComplexPart + if (list.contains(entityComplexPart)) { +- entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); ++ entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entityComplexPart, getSeenFraction(vec3d, entityComplexPart, blockCache, blockPos))); // Paper - actually optimise explosions and use the right entity to calculate the damage + } + } + } else { +- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); ++ entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, getSeenFraction(vec3d, entity, blockCache, blockPos))); // Paper - actually optimise explosions + } + + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled +@@ -289,7 +605,7 @@ public class Explosion { + // CraftBukkit end + } + +- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions ++ double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity, blockCache, blockPos) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions + double d13; + + if (entity instanceof LivingEntity) { +@@ -328,6 +644,9 @@ public class Explosion { + } + } + ++ this.blockCache = null; // Paper - optimise explosions ++ this.chunkPosCache = null; // Paper - optimise explosions ++ this.chunkCache = null; // Paper - optimise explosions + } + + public void finalizeExplosion(boolean particles) { +@@ -547,14 +866,14 @@ public class Explosion { + private BlockInteraction() {} + } + // Paper start - Optimize explosions +- private float getBlockDensity(Vec3 vec3d, Entity entity) { ++ private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions + if (!this.level.paperConfig().environment.optimizeExplosions) { +- return getSeenPercent(vec3d, entity); ++ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions + } + CacheKey key = new CacheKey(this, entity.getBoundingBox()); + Float blockDensity = this.level.explosionDensityCache.get(key); + if (blockDensity == null) { +- blockDensity = getSeenPercent(vec3d, entity); ++ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions; + this.level.explosionDensityCache.put(key, blockDensity); + } + +diff --git a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java +index 0ef9b402d129b072134688c06719a56328581158..1eb259b48bcab6172c15546744eea410c6a3e1fe 100644 +--- a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java ++++ b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java +@@ -26,11 +26,17 @@ public class ExplosionDamageCalculator { + return 1.0F; + } + ++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper + public float getEntityDamageAmount(Explosion explosion, Entity entity) { ++ // Paper start - actually optimise explosions ++ return this.getEntityDamageAmount(explosion, entity, Explosion.getSeenPercent(explosion.center(), entity)); ++ } ++ public float getEntityDamageAmount(Explosion explosion, Entity entity, double seenPercent) { ++ // Paper end - actually optimise explosions + float f = explosion.radius() * 2.0F; + Vec3 vec3 = explosion.center(); + double d = Math.sqrt(entity.distanceToSqr(vec3)) / (double)f; +- double e = (1.0 - d) * (double)Explosion.getSeenPercent(vec3, entity); ++ double e = (1.0 - d) * seenPercent; // Paper - actually optimise explosions + return (float)((e * e + e) / 2.0 * 7.0 * (double)f + 1.0); + } + } -- cgit v1.2.3